Skip to content

Commit

Permalink
Merge pull request #120 from D-VR/add-totp-generator
Browse files Browse the repository at this point in the history
Add totp generator ontop of MFA support
  • Loading branch information
D-VR authored Jul 9, 2024
2 parents eaca276 + da1550c commit 7cc7e1b
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 62 deletions.
48 changes: 31 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ The following command line arguments are available:

```bash
usage: python3 -m syncmymoodle [-h] [--secretservice] [--user USER]
[--password PASSWORD] [--totp TOTP] [--config CONFIG]
[--password PASSWORD] [--totp TOTP] [--totpsecret TOTPSECRET] [--config CONFIG]
[--cookiefile COOKIEFILE] [--courses COURSES]
[--skipcourses SKIPCOURSES]
[--semester SEMESTER] [--basedir BASEDIR]
Expand All @@ -94,13 +94,17 @@ in config.json.

options:
-h, --help show this help message and exit
--secretservice use freedesktop.org's secret service integration for
storing and retrieving account credentials
--secretservice use system's secret service integration for storing and
retrieving account credentials
--secretservicetotpsecret
Save TOTP secret in keyring
--user USER set your RWTH Single Sign-On username
--password PASSWORD set your RWTH Single Sign-On password
--totp TOTP set your RWTH Single Sign-On TOTP provider's serial
number (see
https://idm.rwth-aachen.de/selfservice/MFATokenManager)
--totpsecret TOTPSECRET
(optional) set your RWTH Single Sign-On TOTP provider Secret
--config CONFIG set your configuration file
--cookiefile COOKIEFILE
set the location of a cookie file
Expand Down Expand Up @@ -139,9 +143,11 @@ configuration does:
"user": "", // RWTH SSO username
"password": "", // RWTH SSO password
"totp": "", // RWTH SSO TOTP "Serial Number", format: TOTP0000000A, see https://idm.rwth-aachen.de/selfservice/MFATokenManager
"totpsecret": "", // The TOTP Secret for your TOTP generator (optional)
"basedir": "./", // The base directory where all your files will be synced to
"cookie_file": "./session", // The location of the session/cookie file, which can be used instead of a password.
"use_secret_service": false, // Use the Secret Service integration (see README), instead of a password or a cookie file.
"use_secret_service": false, // Use the system keyring (see README), instead of a password.
"secret_service_store_totp_secret": false, // Store the TOTP secret in the system keyring.
"no_links": false, // Skip links embedded in pages. Warning: This *will* prevent Onlycast videos from being downloaded.
"used_modules": { // Disable downloading certain modules.
"assign": true, // Assignments
Expand All @@ -163,20 +169,27 @@ Command line arguments have a higher priority than configuration files.
You can override any of the options that you have configured in the file
using command line arguments.
## FreeDesktop.org Secret Service integration
### TOTP
*This section is intended for Linux desktop users, as well as users of certain
Unix-like operating systems (FreeBSD, OpenBSD, NetBSD).*
From the RWTH IDM service you will get a TOTP secret which will be used to
generate OTP tokens. The serial number of the TOTP, which can be seen in the
[RWTH IDM Token Manager](https://idm.rwth-aachen.de/selfservice/MFATokenManager),
has to be provided using the `--totp` option or the JSON entry of the same name.
It usually has the format `TOTP12345678`.
You are advised to install and use the optional
[FreeDesktop.org Secret Service integration](#freedesktoporg-secret-service-integration)
to store your password securely if your system supports it - if you're on a modern
Linux desktop-oriented distribution, it most probably does!
The TOTP secret can be specified using the `--totpsecret` option or the JSON
entry of the same name. It can be found in the `otpauth://` link in the secret
argument.
If you have a FreeDesktop.org Secret Service integration compatible keyring
installed, you can store your RWTH SSO credentials in it and use it with
*syncMyMoodle*, which can be particularly useful if you do not like storing
your passwords in plain text files.
## Keyring Integration
You are advised to install and use the optional Keyring integration
to store your password securely if your system supports it, see the
[projects page](https://github.com/jaraco/keyring) for all supported systems.
If you have a compatible keyring installed, you can store your RWTH SSO
credentials in it and use it with *syncMyMoodle*, which can be particularly
useful if you do not like storing your passwords in plain text files.
To do that, you will have to install *syncMyMoodle* with an extra `keyring`
argument:
Expand All @@ -187,8 +200,9 @@ pip3 install syncmymoodle[keyring] # when installing from PyPi
pip3 install .[keyring] # when installing manually
```
You will be asked for your password when using *syncMyMoodle* for the first
time, which you can supply as a parameter or in the configuration file.
You will be asked for your password and TOTP secret when using
*syncMyMoodle* for the first time, which you can supply as a parameter or
in the configuration file.
If everything went alright, you won't need to enter your password again
in the future, as it will be obtained automatically and securely from
Expand Down
2 changes: 2 additions & 0 deletions config.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"user": "",
"password": "",
"totp": "",
"totpsecret": "",
"basedir": "./",
"cookie_file": "./session",
"use_secret_service": false,
"secret_service_store_totp_secret": false,
"no_links": false,
"used_modules": {
"assign": true,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ module = [
"yt_dlp",
"tqdm",
"pdfkit",
"secretstorage",
"keyring",
]
ignore_missing_imports = true
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ install_requires =
quiz =
pdfkit>=0.6.0
keyring =
secretstorage>=3.1.0
keyring>=20.0.0
test =
black
isort
Expand Down
129 changes: 86 additions & 43 deletions syncmymoodle/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@
import base64
import getpass
import hashlib
import hmac
import http.client
import json
import logging
import os
import pickle
import re
import shutil
import struct
import sys
import time
import urllib.parse
from argparse import ArgumentParser
from contextlib import closing
from fnmatch import fnmatchcase
from pathlib import Path
from time import sleep
from typing import TYPE_CHECKING, List
from typing import List

try:
import pdfkit
Expand All @@ -30,19 +32,35 @@
from tqdm import tqdm

try:
import secretstorage
import keyring
except ImportError:
if not TYPE_CHECKING:
# An ignore hint does not work as it would be marked as superfluous
# by mypy if secretstorage is installed.
# Therefore we result to the TYPE_CHECKING constant
secretstorage = None
keyring = None

YOUTUBE_ID_LENGTH = 11

logger = logging.getLogger(__name__)


"""
To add TOTP functionality without adding external dependencies.
Code taken from:
https://github.com/susam/mintotp
"""


def hotp(key, counter, digits=6, digest="sha1"):
key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8))
counter = struct.pack(">Q", counter)
mac = hmac.new(key, counter, digest).digest()
offset = mac[-1] & 0x0F
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF
return str(binary)[-digits:].zfill(digits)


def totp(key, time_step=30, digits=6, digest="sha1"):
return hotp(key, int(time.time() / time_step), digits, digest)


class Node:
def __init__(
self,
Expand Down Expand Up @@ -250,8 +268,12 @@ def get_session_key(soup):
sys.exit(1)

csrf_token = soup.find("input", {"name": "csrf_token"})["value"]
if not self.config.get("totpsecret"):
totp_input = input(f"Enter TOTP for generator {self.config['totp']}:\n")
else:
totp_input = totp(self.config.get("totpsecret"))
print(f"Generated TOTP from provided secret: {totp_input}")

totp_input = input(f"Enter TOTP for generator {self.config['totp']}:\n")
totp_login_data = {
"fudis_otp_input": totp_input,
"_eventId_proceed": "",
Expand All @@ -260,7 +282,7 @@ def get_session_key(soup):

resp4 = self.session.post(resp3.url, data=totp_login_data)

sleep(1) # if we go too fast, we might have our connection closed
time.sleep(1) # if we go too fast, we might have our connection closed
soup = bs(resp4.text, features="html.parser")
if soup.find("input", {"name": "RelayState"}) is None:
logger.critical(
Expand Down Expand Up @@ -1118,11 +1140,16 @@ def main():
description="Synchronization client for RWTH Moodle. All optional arguments override those in config.json.",
)

if secretstorage:
if keyring:
parser.add_argument(
"--secretservice",
action="store_true",
help="use freedesktop.org's secret service integration for storing and retrieving account credentials",
help="Use system's keyring for storing and retrieving account credentials",
)
parser.add_argument(
"--secretservicetotpsecret",
action="store_true",
help="Save TOTP secret in keyring",
)

parser.add_argument(
Expand All @@ -1136,6 +1163,11 @@ def main():
default=None,
help="set your RWTH Single Sign-On TOTP provider's serial number (see https://idm.rwth-aachen.de/selfservice/MFATokenManager)",
)
parser.add_argument(
"--totpsecret",
default=None,
help="(optional) set your RWTH Single Sign-On TOTP provider Secret",
)
parser.add_argument("--config", default=None, help="set your configuration file")
parser.add_argument(
"--cookiefile", default=None, help="set the location of a cookie file"
Expand Down Expand Up @@ -1206,6 +1238,7 @@ def main():
config["user"] = args.user or config.get("user")
config["password"] = args.password or config.get("password")
config["totp"] = args.totp or config.get("totp")
config["totpsecret"] = args.totpsecret or config.get("totpsecret")
config["cookie_file"] = args.cookiefile or config.get("cookie_file", "./session")
config["selected_courses"] = (
args.courses.split(",") if args.courses else config.get("selected_courses", [])
Expand All @@ -1217,8 +1250,11 @@ def main():
)
config["basedir"] = args.basedir or config.get("basedir", "./")
config["use_secret_service"] = (
args.secretservice if secretstorage else None
args.secretservice if keyring else None
) or config.get("use_secret_service")
config["secret_service_store_totp_secret"] = (
args.secretservicetotpsecret if keyring else None
) or config.get("secret_service_store_totp_secret")
config["skip_courses"] = (
args.skipcourses.split(",")
if args.skipcourses
Expand Down Expand Up @@ -1251,44 +1287,51 @@ def main():
"You do not have wkhtmltopdf in your path. Quiz-PDFs are NOT generated"
)

if secretstorage and config.get("use_secret_service"):
if keyring and config.get("use_secret_service"):
if config.get("password"):
logger.critical("You need to remove your password from your config file!")
sys.exit(1)

connection = secretstorage.dbus_init()
collection = secretstorage.get_default_collection(connection)
if collection.is_locked():
collection.unlock()
attributes = {"application": "syncMyMoodle"}
results = list(collection.search_items(attributes))
if len(results) == 0:
if not args.user and not config.get("user"):
print(
"You need to provide your username in the config file or through --user!"
)
sys.exit(1)
if config.get("secret_service_store_totp_secret") and config.get("totpsecret"):
logger.critical("You need to remove your totpsecret from your config file!")
sys.exit(1)

if not args.user and not config.get("user"):
print(
"You need to provide your username in the config file or through --user!"
)
sys.exit(1)

if (
config.get("secretservicetotpsecret")
and not args.totp
and not config.get("totp")
):
print(
"You need to provide your TOTP provider in the config file or through --totp!"
)
sys.exit(1)

config["password"] = keyring.get_password("syncmymoodle", config.get("user"))
if config["password"] is None:
if args.password:
password = args.password
else:
password = getpass.getpass("Password:")
attributes["username"] = config["user"]
item = collection.create_item(
f'{config["user"]}@rwth-aachen.de', attributes, password
keyring.set_password("syncmymoodle", config.get("user"), password)
config["password"] = password

if config.get("secret_service_store_totp_secret"):
config["totpsecret"] = keyring.get_password(
"syncmymoodle", config.get("totp")
)
else:
item = results[0]
if item.is_locked():
"""
item.unlock() returns true if the promt has been dismissed, therefore we
'busy-wait' for false.
"""
while item.unlock():
print("Please confirm to unlock the password if prompted!")
pass
if not config.get("user"):
config["user"] = item.get_attributes().get("username")
config["password"] = item.get_secret().decode("utf-8")
if config["totpsecret"] is None:
if args.totpsecret:
totpsecret = args.totpsecret
else:
totpsecret = getpass.getpass("TOTP-Secret:")
keyring.set_password("syncmymoodle", config.get("totp"), totpsecret)
config["totpsecret"] = totpsecret

if not config.get("user") or not config.get("password"):
logger.critical(
Expand All @@ -1298,7 +1341,7 @@ def main():

if not config.get("totp"):
logger.critical(
"You need to specify your totp generator in the config file or as an argument!"
"You need to specify your TOTP generator in the config file or as an argument!"
)
sys.exit(1)

Expand Down

0 comments on commit 7cc7e1b

Please sign in to comment.