summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndreas Schneider <asn@cryptomilk.org>2019-07-10 17:53:39 +0200
committerAndreas Schneider <asn@cryptomilk.org>2019-07-11 12:06:53 +0200
commite63bd825cc96e6bd67871bc2090808ae5c35fe08 (patch)
tree1babc39dc9a19f82876c8e84bc634718bb26e676
parent06eed2bbdcaa060ec29cff35ad4de39c032a7836 (diff)
downloadcpaste-e63bd825cc96e6bd67871bc2090808ae5c35fe08.tar.gz
cpaste-e63bd825cc96e6bd67871bc2090808ae5c35fe08.tar.xz
cpaste-e63bd825cc96e6bd67871bc2090808ae5c35fe08.zip
Support v2 paste format
-rwxr-xr-xcpaste181
1 files changed, 103 insertions, 78 deletions
diff --git a/cpaste b/cpaste
index 3013c2a..c95b2db 100755
--- a/cpaste
+++ b/cpaste
@@ -4,8 +4,8 @@
#
# A script to paste to https://cpaste.org/
#
-# Copyright (c) 2013 Andreas Schneider <asn@samba.org>
-# Copyright (c) 2013 Alexander Bokovoy <ab@samba.org>
+# Copyright (c) 2013-2019 Andreas Schneider <asn@samba.org>
+# Copyright (c) 2013 Alexander Bokovoy <ab@samba.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -22,7 +22,9 @@
#
#######################################################################
#
-# Requires: python-requests
+# Requires: python3-requests
+# Requires: python3-cryptography
+#
# Optionally requires: python-Pygments
#
@@ -32,10 +34,10 @@ import json
import base64
import zlib
import requests
-from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
-from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
+from cryptography.hazmat.backends import default_backend
from optparse import OptionParser
try:
from pygments.lexers import guess_lexer, guess_lexer_for_filename
@@ -68,10 +70,22 @@ def base58_encode(v):
return (alphabet[0:1] * nPad + string)
-def sjcl_encrypt(plaintext, passphrase):
+def json_encode(d):
+ return json.dumps(d, separators=(',',':')).encode('utf-8')
+
+#
+# The encryption format is described here:
+# https://github.com/PrivateBin/PrivateBin/wiki/Encryption-format
+#
+def privatebin_encrypt(paste_formatter,
+ paste_compress,
+ paste_burn,
+ paste_opendicussion,
+ paste_plaintext,
+ paste_passphrase):
# PBKDF
kdf_salt = bytes(os.urandom(8))
- kdf_iterations = 10000
+ kdf_iterations = 100000
kdf_keysize = 256 # size of resulting kdf_key
backend = default_backend()
@@ -80,60 +94,56 @@ def sjcl_encrypt(plaintext, passphrase):
salt=kdf_salt,
iterations=kdf_iterations,
backend=backend)
- kdf_key = kdf.derive(passphrase)
+ kdf_key = kdf.derive(paste_passphrase)
- # AES GCM
- algo = "aes"
- mode = "gcm"
- associated_data = ""
- tag_size = 128
- ts = int(tag_size / 8)
+ # AES-GCM
+ adata_size = 128
- iv = bytes(os.urandom(16)) # 128 bit
+ cipher_iv = bytes(os.urandom(int(adata_size / 8)))
+ cipher_algo = "aes"
+ cipher_mode = "gcm"
- backend = default_backend()
- cipher = Cipher(algorithms.AES(kdf_key),
- modes.GCM(iv, min_tag_length=ts),
- backend=backend)
- encryptor = cipher.encryptor()
- encryptor.authenticate_additional_data(bytes(associated_data, 'utf-8'))
-
- # compress plaintext with zlib
- compressed_blob = base64.b64encode(zlib.compress(plaintext.encode('utf-8')))
-
- cipher_text = encryptor.update(compressed_blob) + encryptor.finalize()
-
- cipher_text = cipher_text + encryptor.tag[:ts]
-
- # validate
- #decryptor = cipher.decryptor()
- #decryptor.update(cipher_text) + decryptor.finalize()
-
- # JSON object properties
- #
- # iv: random initialization vector, encoded as base64 string
- # v: version number of the SJCL data format, currently always 1
- # iter: number of iterations, by default 1000
- # ks: key size in bits, should ideally be 256, but SJCL also supports 128 bit keys
- # ts: authentication strength in bits, should ideally be 128, but SJCL also supports 64
- # mode: encryption mode, we just switched to gcm by default, but before v1.0 it was ccm
- # adata: optional clear text authentication data (of which we currently make no use)
- # cipher: cipher algorithm, only aes is supported by SJCL for symmetric encryption
- # salt: the salt, encoded as base64 string
- # ct: cipher text, encoded as base64 string
-
- cipher_data = {"iv": base64.b64encode(iv).decode("utf-8"),
- "v": 1,
- "iter": kdf_iterations,
- "ks": kdf_keysize,
- "ts": tag_size,
- "mode": mode,
- "adata": associated_data,
- "cipher": algo,
- "salt": base64.b64encode(kdf_salt).decode("utf-8"),
- "ct": base64.b64encode(cipher_text).decode("utf-8")}
-
- return cipher_data
+ compression_type = "none"
+ if paste_compress:
+ compression_type = "zlib"
+
+ # compress plaintext
+ paste_data = {'paste': paste_plaintext}
+
+ if paste_compress:
+ zobj = zlib.compressobj(wbits=-zlib.MAX_WBITS)
+ paste_blob = zobj.compress(json_encode(paste_data)) + zobj.flush()
+ else:
+ paste_blob = json_encode(paste_data)
+
+ # Associated data to authenticate
+ paste_adata = [
+ [
+ base64.b64encode(cipher_iv).decode("utf-8"),
+ base64.b64encode(kdf_salt).decode("utf-8"),
+ kdf_iterations,
+ kdf_keysize,
+ adata_size,
+ cipher_algo,
+ cipher_mode,
+ compression_type,
+ ],
+ paste_formatter,
+ int(paste_burn),
+ int(paste_opendicussion),
+ ]
+
+ paste_adata_json = json_encode(paste_adata)
+
+ aesgcm = AESGCM(kdf_key)
+ ciphertext = aesgcm.encrypt(cipher_iv, paste_blob, paste_adata_json)
+
+ # Validate
+ # aesgcm.decrypt(cipher_iv, ciphertext, paste_adata_json)
+
+ paste_ciphertext = base64.b64encode(ciphertext).decode("utf-8")
+
+ return paste_adata, paste_ciphertext
def guess_lang_formatter(paste_plaintext, paste_filename=None):
paste_formatter = 'plaintext'
@@ -175,8 +185,9 @@ def main():
parser.add_option("-p", "--password", dest="password",
help="Create a password protected paste", metavar="PASSWORD")
parser.add_option("-e", "--expire",
- action="store", type="string", dest="expire", default="86400",
- help="Expire the paste in X minutes (default: 1 day (1440), valid: 30, 360, 1440, 43200)", metavar="X")
+ action="store", dest="expire", default="1day",
+ choices=["5min", "10min", "1hour", "1day", "1week", "1month", "1year", "never"],
+ help="Expiration time of the paste (default: 1day)")
parser.add_option("-s", "--sourcecode",
action="store_true", dest="source", default=False,
help="Use source code highlighting")
@@ -194,10 +205,10 @@ def main():
paste_url = 'https://cpaste.org'
paste_formatter = 'plaintext'
- paste_expire = 86400
+ paste_compress = True
+ paste_expire = '1day'
paste_opendicussion = 0
paste_burn = 0
- paste_password = None
if options.filename:
f = open(options.filename)
@@ -226,28 +237,37 @@ def main():
paste_formatter = guess_lang_formatter(paste_plaintext,
options.filename)
- if int(options.expire) != 86400:
- paste_expire = int(options.expire) * 60
-
- if options.password:
- paste_password = options.password
+ if options.expire:
+ paste_expire = options.expire
- paste_otp = base64.b64encode(bytes(os.urandom(32)))
+ paste_passphrase = bytes(os.urandom(32))
- cipher_json = sjcl_encrypt(paste_plaintext, paste_otp)
-
- # json payload
- payload = {'data': json.dumps(cipher_json, ensure_ascii=False),
- 'expire': paste_expire,
- 'formatter': paste_formatter,
- 'burnafterreading': paste_burn,
- 'opendiscussion': paste_opendicussion}
+ if options.password:
+ paste_passphrase += options.password
+
+ paste_adata, paste_ciphertext = privatebin_encrypt(paste_formatter,
+ paste_compress,
+ paste_burn,
+ paste_opendicussion,
+ paste_plaintext,
+ paste_passphrase)
+
+ # json payload for the post API
+ # https://github.com/PrivateBin/PrivateBin/wiki/API
+ payload = {
+ "v": 2,
+ "adata": paste_adata,
+ "ct": paste_ciphertext,
+ "meta": {
+ "expire": paste_expire,
+ }
+ }
# http content type
headers = {'X-Requested-With': 'JSONHttpRequest'}
r = requests.post(paste_url,
- data=payload,
+ data=json_encode(payload),
headers=headers)
r.raise_for_status()
@@ -264,11 +284,16 @@ def main():
sys.exit(1)
paste_id = result['id']
+ paste_url_id = result['url']
paste_deletetoken = result['deletetoken']
print('Delete paste: %s/?pasteid=%s&deletetoken=%s' % (paste_url, paste_id, paste_deletetoken))
print('')
- print('### Paste (%s): %s/?%s#%s' % (paste_formatter, paste_url, paste_id, paste_otp.decode('utf-8')))
+ print('### Paste (%s): %s%s#%s' %
+ (paste_formatter,
+ paste_url,
+ paste_url_id,
+ base58_encode(paste_passphrase).decode('utf-8')))
sys.exit(0)