commit a46f03ec8304d397125242b32c25e413bfcae8f5 Author: r4sas Date: Sat Feb 18 21:00:40 2017 +0300 initial upload diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6424f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +*.pyc +venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..33c957d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +PBinCLI +======== + +PrivateBin CLI (in development) diff --git a/pbincli.py b/pbincli.py new file mode 100755 index 0000000..52edc12 --- /dev/null +++ b/pbincli.py @@ -0,0 +1,40 @@ +#! /usr/bin/env python3 +import os +import sys +import argparse + +import pbincli.actions +from pbincli.utils import PBinCLIException + +def main(): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(title="actions", help="List of commands") + # a send command + send_parser = subparsers.add_parser("send", description="Send data to PrivateBin instance", usage="""%(prog)s --cert data/user_at_mail.i2p.crt --private-key data/priv_key.pem --signer-id user@mail.i2p""") + send_parser.add_argument("-b", "--burn", default=False, action="store_true", help="Burn after reading") + send_parser.add_argument("-d", "--discus", default=False, action="store_true", help="Open discussion") + send_parser.add_argument("-e", "--expire", default="1day", action="store", help="Expiration of paste") + send_parser.add_argument("-f", "--format", default="plaintext", action="store", help="Format of text paste") + send_parser.add_argument("-p", "--password", default=None, help="RSA private key (default: data/priv_key.pem)") + send_parser.add_argument("filename", type=argparse.FileType('r'), help="Filename (example: image.jpg)") + send_parser.set_defaults(func=pbincli.actions.send) + '''# a get command + get_parser = subparsers.add_parser("get", description="Recieve data from PrivateBin instance", usage="""echo $YOUR_PASSWORD | %(prog)s --netdb /path/to/netDb --private-key data/priv_key.pem --outfile output/i2pseeds.su3 --signer-id user@mail.i2p""") + get_parser.add_argument("--signer-id", required=True, help="Identifier of certificate (example: user@mail.i2p)") + get_parser.add_argument("--private-key", default="data/priv_key.pem", help="RSA private key (default: data/priv_key.pem)") + get_parser.add_argument("-o", "--outfile", default="output/i2pseeds.su3", help="Output file (default: output/i2pseeds.su3)") + get_parser.add_argument("--netdb", required=True, help="Path to netDb folder (example: ~/.i2pd/netDb)") + get_parser.set_defaults(func=pyseeder.actions.reseed)''' + # parse arguments + args = parser.parse_args() + if hasattr(args, "func"): + try: + args.func(args) + except PBinCLIException as pe: + print("PBinCLI error: {}".format(pe)) + sys.exit(1) + else: + parser.print_help() + +if __name__ == "__main__": + main() diff --git a/pbincli/__init__.py b/pbincli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pbincli/actions.py b/pbincli/actions.py new file mode 100644 index 0000000..3c229e9 --- /dev/null +++ b/pbincli/actions.py @@ -0,0 +1,7 @@ +"""Action functions for argparser""" +import pbincli.sjcl_gcm +import pbincli.actions +from pbincli.utils import PBinCLIException, check_readable, check_writable + +def send(args): + print("Meow!") diff --git a/pbincli/sjcl_gcm.py b/pbincli/sjcl_gcm.py new file mode 100644 index 0000000..ad1b8e1 --- /dev/null +++ b/pbincli/sjcl_gcm.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + + +Edited SJCL.py library (https://github.com/berlincode/sjcl/) + for AES.MODE_GCM support, work begin. + + pip3.5 install pycryptodome + + + +""" + +from Crypto.Hash import SHA256, HMAC +#pip3.5 install pycryptodome +from Crypto.Protocol.KDF import PBKDF2 +from Crypto.Cipher import AES +from Crypto.Random import get_random_bytes +import base64 + + +def truncate_iv(iv, ol, tlen): # ol and tlen in bits + ivl = len(iv) # iv length in bytes + ol = (ol - tlen) // 8 + + # "compute the length of the length" (see gcm.js) + L = 2 + while (L < 4) and ((ol >> (8*L))) > 0: + L += 1 + if L < 15 - ivl: + L = 15 - ivl + + return iv[:(15-L)] + + +def check_mode_gcm(): + # checks if pycrypto has support for gcm + try: + AES.MODE_GCM + except: + raise Exception( + "Pycrypto does not seem to support MODE_gcm. " + + "You need a version >= 2.7a1 (or a special branch)." + ) + + +class SJCL(object): + + def __init__(self): + self.salt_size = 8 # bytes + self.tag_size = 16 # bytes + self.mac_size = 16 # bytes; mac = message authentication code (MAC) + self.prf = lambda p, s: HMAC.new(p, s, SHA256).digest() + + def decrypt(self, data, passphrase): + check_mode_gcm() # check gcm support + + if data["cipher"] != "aes": + raise Exception("only aes cipher supported") + print(data["mode"]) + if data["mode"] != "gcm": + raise Exception("unknown mode(!=gcm)") + + if data["adata"] != "": + raise Exception("additional authentication data not equal ''") + + if data["v"] != 1: + raise Exception("only version 1 is currently supported") + + if data["ts"] != self.tag_size * 8: + raise Exception("desired tag length != %d" % (self.tag_size * 8)) + + salt = base64.b64decode(data["salt"]) + + # print "salt", hex_string(salt) + if len(salt) != self.salt_size: + raise Exception("salt should be %d bytes long" % self.salt_size) + + dkLen = data["ks"]//8 + if dkLen != 16: + raise Exception("key length should be 16 bytes") + + key = PBKDF2( + passphrase, + salt, + count=data['iter'], + dkLen=dkLen, + prf=self.prf + ) +# print "key", hex_string(key) + + ciphertext = base64.b64decode(data["ct"]) + iv = base64.b64decode(data["iv"]) +# print AES.block_size + + nonce = truncate_iv(iv, len(ciphertext)*8, data["ts"]) + + # split tag from ciphertext (tag was simply appended to ciphertext) + mac = ciphertext[-(data["ts"]//8):] +# print len(ciphertext) + ciphertext = ciphertext[:-(data["ts"]//8)] +# print len(ciphertext) +# print len(tag) + +# print "len", len(nonce) + cipher = AES.new(key, AES.MODE_GCM, nonce) + plaintext = cipher.decrypt(ciphertext) + print(mac) +# cipher.verify(mac) + + return plaintext + + def encrypt(self, plaintext, passphrase, count=1000, dkLen=16): + # dkLen = key length in bytes + + check_mode_gcm() # check gcm support + + salt = get_random_bytes(self.salt_size) + iv = get_random_bytes(16) # TODO dkLen? + + key = PBKDF2(passphrase, salt, count=count, dkLen=dkLen, prf=self.prf) + + # TODO plaintext padding? + nonce = truncate_iv(iv, len(plaintext) * 8, self.tag_size * 8) + # print len(nonce) + + cipher = AES.new( + key, + AES.MODE_GCM, + nonce, + mac_len=self.mac_size + ) + + ciphertext = cipher.encrypt(plaintext) + mac = cipher.digest() + + ciphertext = ciphertext + mac + + return { + "salt": base64.b64encode(salt), + "iter": count, + "ks": dkLen*8, + "ct": base64.b64encode(ciphertext), + "iv": base64.b64encode(iv), + "cipher": "aes", + "mode": "gcm", + "adata": "", + "v": 1, + "ts": self.tag_size * 8 + } diff --git a/pbincli/utils.py b/pbincli/utils.py new file mode 100644 index 0000000..93e4bf0 --- /dev/null +++ b/pbincli/utils.py @@ -0,0 +1,15 @@ +"""Various code""" +import os + +class PBinCLIException(Exception): + pass + +def check_readable(f): + """Checks if path exists and readable""" + if not os.path.exists(f) or not os.access(f, os.R_OK): + raise PBinCLIException("Error accessing path: {}".format(f)) + +def check_writable(f): + """Checks if path is writable""" + if not os.access(os.path.dirname(f) or ".", os.W_OK): + raise PBinCLIException("Path is not writable: {}".format(f)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..acdfd20 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pycryptodome