Commit 887a07f1 authored by Leonard Marschke's avatar Leonard Marschke
Browse files

initial student commit

parents
Loading
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+153 −0
Original line number Diff line number Diff line

# Created by https://www.gitignore.io/api/python,pycharm

### PyCharm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
#
mailbox/

# User-specific stuff:
.idea

crypto.conf

## File-based project format:
*.iws

## Plugin-specific files:

# IntelliJ
/out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Ruby plugin and RubyMine
/.rakeTasks

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

### PyCharm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
.idea/sonarlint

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit tests / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
.pytest_cache/
nosetests.xml
coverage.xml
*.cover
.hypothesis/

# Translations
*.mo
*.pot

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule.*

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/


# End of https://www.gitignore.io/api/python,pycharm

.gitlab-ci.yml

0 → 100644
+33 −0
Original line number Diff line number Diff line
image: python:3.6

stages:
  - test
  - pack
  - publish

variables:
  DOCKER_DRIVER: overlay2
  CI_REGISTRY_IMAGE: 'isec_broken_rsa'

style:
  stage: test
  script:
    - apt-get update
    - apt-get install -y libmpfr-dev gmpc-dev libgmp-dev libmpc-dev
    - wget https://sre18.pages.rechenknecht.net/misc/pylintrc -O .pylintrc
    - pip3 install -r requirements.txt
    - pip3 install pylint
    - pylint rsa.py


test:
  stage: test
  script:
    - apt-get update
    - apt-get install -y libmpfr-dev gmpc-dev libgmp-dev libmpc-dev
    - pip3 install -r requirements.txt
    - python3 rsa.py key-gen
    - echo "Sampletext" > originaltext
    - python3 rsa.py encrypt `cat originaltext` > encrypted
    - python3 rsa.py decrypt `cat encrypted` > decrypted
    - diff decrypted originaltext

requirements.txt

0 → 100644
+1 −0
Original line number Diff line number Diff line
gmpy2

rsa.py

0 → 100644
+255 −0
Original line number Diff line number Diff line
# pylint: disable=invalid-name,too-many-return-statements

import secrets
import sys
import random
import time

import gmpy2

INT_BYTES_ARGS = {
    'byteorder': 'little',
    'signed': False,
}
KEY_BYTES = 128


class Config:
    prime1 = None
    prime2 = None
    privateKey = None
    publicKey = None
    fPath = None

    def __init__(self, path):
        self.fPath = path
        try:
            with open(path) as f:
                lines = f.readlines()
        except IOError:
            return

        for key, val in enumerate(lines):
            if key == 0:
                self.prime1 = int(val)
            elif key == 1:
                self.prime2 = int(val)
            elif key == 2:
                self.publicKey = int(val)
            elif key == 3:
                self.privateKey = int(val)

    def valid(self):
        if not isinstance(self.prime1, int):
            return False

        if not isinstance(self.prime2, int):
            return False

        if not isinstance(self.privateKey, int):
            return False

        if not isinstance(self.publicKey, int):
            return False

        return True

    def has_primes(self):
        if not isinstance(self.prime1, int):
            return False

        if not isinstance(self.prime2, int):
            return False

        return True

    def has_pub_key(self):
        return isinstance(self.publicKey, int)

    def get_prime_product(self):
        return self.prime1 * self.prime2

    def store(self):
        target = open(self.fPath, 'w')

        target.truncate()

        target.write(str(self.prime1) + "\n")
        target.write(str(self.prime2) + "\n")
        target.write(str(self.publicKey) + "\n")
        target.write(str(self.privateKey))


def is_prime(num, runs=40):
    """
    Implements Miller Rabin (non deterministic)

    try to falsify num's primality 40 times by default, see
    https://stackoverflow.com/questions/6325576/how-many-iterations-of-rabin-miller-should-i-use-for-cryptographic-safe-primes

    :param num: Number to test
    :param runs: How often to run miller rabin
    :return: True if num is (probably) a prime number.
    """

    # Some checks for small (and even) numbers to speed up computation
    if num < 2:
        return False
    if num < 4:
        return True
    if not num % 2:
        return False
    if num < 9:
        return True
    if not num % 3:
        return False

    # calculate n - 1 = d * 2^j
    d = num - 1
    j = 0
    while d % 2 == 0:
        d = d // 2
        j += 1

    for _ in range(runs):
        a = random.randrange(2, num - 1)
        v = pow(a, d, num)  # a ^ d % num

        # first test
        if v in [1, num - 1]:  # if v == 1, it is probably a prime number
            continue

        # second test
        i = 0
        while v != (num - 1):
            if i == j - 1:
                return False
            i += 1
            v = (v ** 2) % num
    return True


def gen_primes():
    random.seed(time.time())
    while True:
        prime = bytearray(secrets.token_bytes(KEY_BYTES))  # 1024 bits of key length
        if prime[-1] < (1 << 7):  # to make sure that our prime numbers are big enough (cryptographically secure)
            continue
        # convert byte array to int
        prime = int.from_bytes(prime, **INT_BYTES_ARGS)
        if is_prime(prime) and is_prime(prime + 2):  # check prime (offset as well)
            return prime, prime + 2  # prime is fine


def gcd(a, b):
    while b != 0:
        a, b = b, a % b
    return a


def modinv(a, m):
    return gmpy2.invert(a, m).numerator  # pylint: disable=c-extension-no-member


def key_gen(config):
    if config.has_primes():
        print('Found primes in config file!')
    else:
        print('Generating new primes...')
        print('This takes some time due to the "slowness" of Python...')
        config.prime1, config.prime2 = gen_primes()

    print('Generating public key...')
    p = (config.prime1 - 1) * (config.prime2 - 1)

    if not config.has_pub_key():
        # Make sure e is not a coprime of our prime numbers
        g = None
        while g != 1:
            e = random.randrange(1, p)
            g = gcd(e, p)
    else:
        e = config.publicKey

    print('Generating private key...')
    config.privateKey = modinv(e, p)
    config.publicKey = e

    config.store()


def encrypt(config):
    if len(sys.argv) < 3:
        print('usage: %s encrypt message' % sys.argv[0])
        exit(254)

    message = " ".join(sys.argv[2:])

    # convert message to bitarray
    cleartext = message.encode('utf-8')

    ciphertext = []

    # encrypt each message part
    for start in range(0, len(cleartext), KEY_BYTES):
        text_num = int.from_bytes(cleartext[start:start + KEY_BYTES], **INT_BYTES_ARGS)
        ciphertext.append(str(pow(text_num, config.publicKey, config.get_prime_product())))

    print('%'.join(ciphertext))


def decrypt(config):
    if len(sys.argv) < 3:
        print('usage: %s decrypt message' % sys.argv[0])
        exit(254)

    message = sys.argv[2]
    # decode encrypted message and split it to get every block
    chunks = message.split('%')

    decrypted_bytes = b''

    # read the message into a bitarray
    for chunk in chunks:
        # decrypt each block
        decrypted = pow(int(chunk), config.privateKey, config.get_prime_product())
        decrypted_bytes += int.to_bytes(decrypted, KEY_BYTES, **INT_BYTES_ARGS)

    # remove appended padding 0-bytes
    for key in range(len(decrypted_bytes) - 1, 0, -1):  # we (hopefully) do not have a padding only chunk
        if decrypted_bytes[key] != 0:
            decrypted_bytes = decrypted_bytes[0:key + 1]
            break

    # convert bitarray to string
    print(decrypted_bytes.decode('utf-8'))


def main():
    # if we got less than 2 arguments the program is not called in a valid way... TODO refactor to argparse
    if len(sys.argv) < 2:
        print('usage: %s key-gen|encrypt|decrypt' % sys.argv[0])
        exit(254)

    # read config file
    config = Config('./crypto.conf')

    if sys.argv[1] == 'key-gen':
        key_gen(config)
    elif sys.argv[1] == 'encrypt':
        encrypt(config)
    elif sys.argv[1] == 'decrypt':
        decrypt(config)
    else:
        print('Invalid command!')
        exit(1)

    # store our possibly modified configuration
    config.store()

    # all task are successfully executed! Exit with code 0
    exit(0)


if __name__ == '__main__':
    main()