haste.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. #!/usr/bin/env python
  2. # initially stolen from: https://github.com/jirutka/haste-client
  3. # adapted to be only for python 3
  4. # added get snippet feature and crypt snippet feature
  5. # change the server url in CONFIG if needed
  6. """
  7. haste - a CLI client for Haste server.
  8. Usage:
  9. haste send [-r] [--password=<pwd>] [FILE]
  10. haste get [--password=<pwd>] KEY
  11. Options:
  12. -r --raw Return a URL to the raw paste data.
  13. --password=<pwd> Encrypt/decrypt message using <pwd> as password.
  14. -h --help Show this message.
  15. -v --version Show version.
  16. If FILE is not specified, haste will read from standard input.
  17. """
  18. import os
  19. from base64 import b64decode, b64encode
  20. import json
  21. import sys
  22. import docopt
  23. import requests
  24. from cryptography.hazmat.primitives import hashes
  25. from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
  26. from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
  27. from cryptography.hazmat.backends import default_backend
  28. _BACKEND = default_backend()
  29. __version__ = '2.0.1'
  30. CONFIG = {
  31. 'server_url': "https://hastebin.com",
  32. 'timeout': 3
  33. }
  34. def main(**kwargs):
  35. """ main function: do actions following args """
  36. if kwargs['KEY'] and kwargs['get']:
  37. data = get_snippet(kwargs['KEY'], CONFIG['server_url'], CONFIG['timeout'])
  38. if kwargs['--password']:
  39. data = decrypt(kwargs['--password'], json.loads(data)).decode("utf-8")
  40. print(data)
  41. elif kwargs['send']:
  42. data = read_file(kwargs['FILE']) if kwargs['FILE'] else read_stdin()
  43. if kwargs['--password']:
  44. data = json.dumps(encrypt(kwargs['--password'], data))
  45. url = create_snippet(data, CONFIG['server_url'], CONFIG['timeout'], kwargs['--raw'])
  46. print(url)
  47. else:
  48. print(docopt.docopt(__doc__))
  49. def get_snippet(dockey, baseurl, timeout):
  50. """ get a snippet from the server """
  51. try:
  52. url = baseurl + "/raw/" + dockey
  53. response = requests.get(url, timeout=float(timeout))
  54. except requests.exceptions.Timeout:
  55. exit("Error: connection timed out")
  56. return response.text
  57. def create_snippet(data, baseurl, timeout, raw):
  58. """
  59. Creates snippet with the given data on the haste server specified by the
  60. baseurl and returns URL of the created snippet.
  61. """
  62. try:
  63. url = baseurl + "/documents"
  64. response = requests.post(url, data.encode('utf-8'), timeout=float(timeout))
  65. except requests.exceptions.Timeout:
  66. exit("Error: connection timed out")
  67. dockey = json.loads(response.text)['key']
  68. return baseurl + ("/raw/" if raw else "/") + dockey
  69. def read_stdin():
  70. """ joins lines of stdin into a single string """
  71. return "".join(sys.stdin.readlines()).strip()
  72. def read_file(path):
  73. """ reads lines of a file and joins them into a single string """
  74. try:
  75. with open(path, 'r') as text_file:
  76. return "".join(text_file.readlines()).strip()
  77. except IOError:
  78. exit("Error: file '%s' is not readable!" % path)
  79. def decrypt(pwd, data):
  80. """ Decrypt using password and input json """
  81. ct = b64decode(data['ct'])
  82. salt = b64decode(data['salt'])
  83. tag_start = len(ct) - data['ts'] // 8
  84. tag = ct[tag_start:]
  85. ciphertext = ct[:tag_start]
  86. mode_class = getattr(modes, data['mode'].upper())
  87. algo_class = getattr(algorithms, data['cipher'].upper())
  88. kdf = _kdf(data['ks'], iters=data['iter'], salt=salt)[0]
  89. key = kdf.derive(bytes(pwd, "utf-8"))
  90. cipher = Cipher(
  91. algo_class(key),
  92. mode_class(
  93. b64decode(data['iv']),
  94. tag,
  95. min_tag_length=data['ts'] // 8
  96. ),
  97. backend=_BACKEND
  98. )
  99. dec = cipher.decryptor()
  100. return dec.update(ciphertext) + dec.finalize()
  101. def _kdf(keysize=128, iters=256000, salt=None):
  102. """ Returns a key derivation function: used to create a strong key based on the input """
  103. kdf_salt = salt or os.urandom(8)
  104. kdf = PBKDF2HMAC(
  105. algorithm=hashes.SHA256(),
  106. length=keysize // 8,
  107. salt=kdf_salt,
  108. iterations=iters,
  109. backend=_BACKEND
  110. )
  111. return kdf, kdf_salt
  112. def encrypt(pwd, plaintext, mode='gcm', keysize=128, tagsize=128, iters=256000):
  113. """ Encrypt plain text using password. Outputs json """
  114. ts = tagsize // 8
  115. mode_class = getattr(modes, mode.upper())
  116. algo_class = getattr(algorithms, 'AES')
  117. iv = os.urandom(16)
  118. kdf, salt = _kdf(keysize, iters)
  119. bpwd = str.encode(pwd)
  120. key = kdf.derive(bpwd)
  121. cipher = Cipher(
  122. algo_class(key),
  123. mode_class(iv, min_tag_length=ts),
  124. backend=_BACKEND
  125. )
  126. enc = cipher.encryptor()
  127. btext = str.encode(plaintext)
  128. ciphertext = enc.update(btext) + enc.finalize()
  129. output = {
  130. "v": 1,
  131. "iv": b64encode(iv).decode("utf-8"),
  132. "salt": b64encode(salt).decode("utf-8"),
  133. "ct": b64encode(ciphertext + enc.tag[:ts]).decode("utf-8"),
  134. "iter": iters,
  135. "ks": keysize,
  136. "ts": tagsize,
  137. "mode": mode,
  138. "cipher": 'aes',
  139. "adata": ""
  140. }
  141. return output
  142. DOC = docopt.docopt(__doc__, version='haste ' + __version__)
  143. if __name__ == "__main__":
  144. main(**DOC)