Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Develop in Main #140

Merged
merged 5 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Steps to contribute:
2. Checkout develop branch `git checkout develop`
3. Create new feature branch from develop `git checkout -b <my_new_feature_branch>`
4. Install dependencies `npm ci` - you need node package manager npm installed
5. Run `npm useChrome` or `npm useFF` to select the browser you are developing for - this will copy the corresponding manifest.json
5. Run `npm run useChrome` or `npm run useFF` to select the browser you are developing for - this will copy the corresponding manifest.json
6. Run `npm run dev` while developing. This is will compile sass and ts files and watch for changes in your working tree.
7. Load the ./build directory as an unpacked extension in your browser
8. Run tests locally before committing code `npm run test`
Expand Down
29 changes: 29 additions & 0 deletions src/background.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'
import * as credentials from './modules/credentials'
import * as otp from './modules/otp'
import * as owaFetch from './modules/owaFetch'
import * as opalInline from './modules/opalInline'
import { isFirefox } from './modules/firefoxCheck'
Expand Down Expand Up @@ -225,6 +226,34 @@ chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
// Asynchronous response
credentials.deleteUserData(request.platform).then(sendResponse) // Response can probably be ignored
return true // required for async sendResponse
case 'get_totp':
// Asynchronous response
otp.getTOTP(request.platform).then(sendResponse)
return true // required for async sendResponse
case 'get_iotp':
// Asynchronous response
if (!request.indexes) return sendResponse(undefined)
otp.getIOTP(request.platform, ...request.indexes).then(sendResponse)
return true // required for async sendResponse
case 'set_otp':
// Asynchronous response
switch (request.otpType) {
case 'totp':
if (!request.secret) return sendResponse(false)
credentials.setUserData({ user: 'totp', pass: request.secret }, (request.platform ?? 'zih') + '-totp').then(() => {
credentials.deleteUserData((request.platform ?? 'zih') + '-iotp').then(() => sendResponse(true))
})
return true // required for async sendResponse

case 'iotp':
if (!request.secret) return sendResponse(false)
credentials.setUserData({ user: 'iotp', pass: request.secret }, (request.platform ?? 'zih') + '-iotp').then(() => {
credentials.deleteUserData((request.platform ?? 'zih') + '-totp').then(() => sendResponse(true))
})
return true // required for async sendResponse

default: return sendResponse(false)
}
/* OWA */
case 'enable_owa_fetch':
owaFetch.enableOWAFetch().then(sendResponse)
Expand Down
34 changes: 30 additions & 4 deletions src/contentScripts/login/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@ export interface CookieSettings {
usesIdp?: boolean;
}

interface OTPSettings {
input: HTMLInputElement | null;
submitButton?: HTMLElement | null;
type: 'totp' | 'iotp';
indexes?: number[];
}

export interface LoginFields {
usernameField: HTMLInputElement;
passwordField: HTMLInputElement;
submitButton?: HTMLElement;
otpSettings?: OTPSettings;
}

// This is the default lifetime for the logout cookie in minutes.
Expand Down Expand Up @@ -147,15 +155,33 @@ export abstract class Login {

const avail = await this.loginFieldsAvailable().catch(() => { })
if (typeof avail === 'boolean' && !avail) return
if (typeof avail === 'object') {
if (!avail.usernameField || !avail.passwordField) return
else loginFields = avail
}
if (typeof avail === 'object') loginFields = avail

// Fill the otp
// If we clicked the submit button, we can return here
if (loginFields?.otpSettings && (await this.fillOtp(loginFields.otpSettings))) return

await this.onLogin()
await this.login(userData, loginFields)
}

async fillOtp (otpSettings: OTPSettings): Promise<boolean> {
if (!otpSettings.input) return false

let otp: string | undefined
if (otpSettings.type === 'totp') {
otp = await chrome.runtime.sendMessage({ cmd: 'get_totp', platform: this.platform })
} else if (otpSettings.type === 'iotp') {
otp = await chrome.runtime.sendMessage({ cmd: 'get_iotp', platform: this.platform, indexes: otpSettings.indexes })
}

if (!otp || otp.length === 0) return false

this.fakeInput(otpSettings.input, otp)
otpSettings.submitButton?.click()
return !!otpSettings.submitButton
}

fakeInput (input: HTMLInputElement, value: string) {
// Inspired by how the Bitwarden extension does it
// https://github.com/bitwarden/clients/blob/master/apps/browser/src/content/autofill.js#L346
Expand Down
33 changes: 29 additions & 4 deletions src/contentScripts/login/idp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ const cookieSettings: CookieSettings = {
async additionalFunctionsPostCheck (): Promise<void> {
this.confirmData()
this.outdatedRequest()
this.selectOTPType()
}

confirmData () {
// Check if this is the consense page
if (!document.getElementById('generalConsentDiv')) return

// Click the button
const button = document.querySelector('input[type="submit"][name="_eventId_proceed"]')
if (button) (button as HTMLInputElement).click()
const button = document.querySelector('input[type="submit"][name="_eventId_proceed"]') as HTMLInputElement | null
button?.click()
}

outdatedRequest () {
Expand All @@ -40,16 +41,40 @@ const cookieSettings: CookieSettings = {
// We don't know where the user tried to login, so we can't jsut redirect to Opal/etc
}

selectOTPType () {
if (!document.getElementById('fudis_selected_token_ids_input')) return

const button = document.querySelector('button[type="submit"][name="_eventId_proceed"]') as HTMLButtonElement | null
button?.click()
}

async findCredentialsError (): Promise<boolean | HTMLElement | Element | null> {
return document.querySelector('.content p font[color="red"]')
}

async loginFieldsAvailable (): Promise<boolean | LoginFields> {
return {
const fields: LoginFields = {
usernameField: document.getElementById('username') as HTMLInputElement,
passwordField: document.getElementById('password') as HTMLInputElement,
submitButton: document.querySelector('button[name="_eventId_proceed"][value="Login"]') as HTMLButtonElement
submitButton: document.querySelector('button[name="_eventId_proceed"][type="submit"]') as HTMLButtonElement
}

const otpInput = document.getElementById('fudis_otp_input') as HTMLInputElement | null
if (otpInput) {
const indexesText = otpInput.parentElement?.parentElement?.querySelector('td:first-of-type')?.textContent?.trim()
// find number & number | remove whole match | to numbers | to zero based (first index is 0)
const indexes = indexesText?.match(/(\d+) & (\d+)/)?.slice(1, 3).map((x) => Number.parseInt(x, 10) - 1)

fields.otpSettings = {
input: otpInput,
submitButton: document.querySelector('button[name="_eventId_proceed"][type="submit"]') as HTMLButtonElement | null,
type: indexesText?.toLocaleLowerCase().includes('totp') ? 'totp' : 'iotp',
indexes: indexes && indexes.length > 0 ? indexes : undefined
}
console.log(fields)
}

return fields
}

async findLogoutButtons (): Promise<(HTMLElement|Element|null)[] | NodeList | null> {
Expand Down
24 changes: 24 additions & 0 deletions src/contentScripts/other/otpSnatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const qrAvailable = !!document.getElementById('qr-code')
const seedLink = document.querySelector('#seed-link a[href^="otpauth://totp/"]')

const indexedAvailable = document.getElementById('indexed-secret')

if (qrAvailable && seedLink && showWarning()) {
const seed = seedLink.getAttribute('href')
if (seed) {
const secret = new URL(seed).searchParams.get('secret')
chrome.runtime.sendMessage({ cmd: 'set_otp', otpType: 'totp', secret, platform: 'zih' })
}
} else if (!!indexedAvailable && showWarning()) {
const cols = Array.from(indexedAvailable.querySelectorAll('tr:nth-of-type(2) td'))
// Maybe the ZIH will change the number of chars in the future
// Update it here!
if (cols.length === 25) {
const secret = cols.map((col) => (col as HTMLTableCellElement).innerText).reduce((acc, cur) => acc + cur, '')
chrome.runtime.sendMessage({ cmd: 'set_otp', otpType: 'iotp', secret, platform: 'zih' })
}
}

function showWarning (): boolean {
return confirm('TUfast kann diesen 2-Faktor-Code für dich speichern und automatisch an den entsprechenden Stellen einf\u00fcgen. Dies geht jedoch gegen den Sinn eines zweiten Faktors und ist noch in Entwicklung.\n\nSPEICHERE DIR DEN CODE UND DIE RECOVERY CODES AUF JEDEN FALL AUCH AN EINER ANDEREN STELLE!\n\nSoll TUfast für dich die 2-Faktor-Authentifizierung \u00fcbernehmen?')
}
34 changes: 16 additions & 18 deletions src/freshContent/popup/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,44 +42,44 @@
<span class="slider round"></span>
</label>
</div-->
<a href="https://selma.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" id="selma">
<a href="https://selma.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" title="Selma" id="selma">
<img class="settings-img" src="../../assets/icons/selma.png">
</a>
<a href="https://eportal.med.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1; display: none" id="eportal">
<a href="https://eportal.med.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1; display: none" title="ePortal" id="eportal">
<img class="settings-img" src="../../assets/icons/ePortal.png">
</a>
<a href="https://elearning.med.tu-dresden.de/moodle/my/" target="_blank" class="settings-section-icon" style="flex: 1; display: none" id="moodle">
<a href="https://elearning.med.tu-dresden.de/moodle/my/" target="_blank" class="settings-section-icon" style="flex: 1; display: none" title="Moodle" id="moodle">
<img class="settings-img" src="../../assets/icons/moodle.png">
</a>
<a href="https://bildungsportal.sachsen.de/opal/" target="_blank" class="settings-section-icon" style="flex: 1" id="opal">
<a href="https://bildungsportal.sachsen.de/opal/" target="_blank" class="settings-section-icon" style="flex: 1" title="OPAL" id="opal">
<img class="settings-img" src="../../assets/icons/OPAL.png">
</a>
<a href="https://qis.dez.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" id="qis">
<a href="https://qis.dez.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" title="QIS" id="qis">
<img class="settings-img invert" src="../../assets/icons/his.png">
</a>
<a href="https://matrix.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" id="matrix">
<a href="https://matrix.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" title="Matrix Chat" id="matrix">
<img class="settings-img" src="../../assets/icons/element.svg">
</a>
<a href="https://msx.tu-dresden.de/owa/#path=/mail" target="_blank" class="settings-section-icon" style="flex: 1" id="msx">
<a href="https://msx.tu-dresden.de/owa/#path=/mail" target="_blank" class="settings-section-icon" style="flex: 1" title="TU Mail" id="msx">
<img class="settings-img" src="../../assets/icons/owa.png">
</a>
<a href="https://www.slub-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" id="slub">
<a href="https://www.slub-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" title="SLUB" id="slub">
<img class="settings-img" style="height: 21px; width: auto;" src="../../assets/icons/slub.png">
</a>
<a href="https://datashare.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" id="cloud">
<a href="https://datashare.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" title="Datashare" id="cloud">
<img class="settings-img invert" src="../../assets/icons/cloud.png">
</a>
<a href="https://jexam.inf.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" id="je">
<a href="https://jexam.inf.tu-dresden.de/" target="_blank" class="settings-section-icon" style="flex: 1" title="jExam" id="je">
<img class="settings-img invert" src="../../assets/icons/jE.png">
</a>
<a href="https://gitlab.mn.tu-dresden.de" target="_blank" class="settings-section-icon" style="flex: 1" id="gitlab">
<a href="https://gitlab.mn.tu-dresden.de" target="_blank" class="settings-section-icon" style="flex: 1" title="GitLab" id="gitlab">
<img class="settings-img" src="../../assets/icons/gitlab.png">
</a>
<a href="https://zep.psych.tu-dresden.de/orsee/public/index.php" target="_blank" class="settings-section-icon"
<a href="https://zep.psych.tu-dresden.de/orsee/public/index.php" target="_blank" class="settings-section-icon" title="orsee"
style="flex: 1" id="orsee">
<img class="settings-img invert" src="../../assets/icons/TUD_1.png">
</a>
<a href="https://geoportal.sachsen.de/cps/karte.html?showmap=true" target="_blank" class="settings-section-icon"
<a href="https://geoportal.sachsen.de/cps/karte.html?showmap=true" target="_blank" class="settings-section-icon" title="Geoportal"
style="flex: 1" id="geoportal">
<img class="settings-img" src="../../assets/icons/sachsenatlas.ico">
</a>
Expand All @@ -91,15 +91,15 @@
style="flex: 1" id="nautos">
<img class="settings-img invert" style="height: 23px; width: auto;" src="../../assets/icons/nautos.png">
</a>
<a href="https://discord.gg/5H5RxTQS4s" target="_blank" class="settings-section-icon"
<a href="https://discord.gg/5H5RxTQS4s" target="_blank" class="settings-section-icon" title="Informatik Discord"
style="flex: 1" id="info_discord">
<img class="settings-img" src="../../assets/icons/discord.svg">
</a>
<a href="https://www.studentenwerk-dresden.de/mensen/speiseplan/" target="_blank" class="settings-section-icon"
<a href="https://www.studentenwerk-dresden.de/mensen/speiseplan/" target="_blank" class="settings-section-icon" title="Mensa"
style="flex: 1" id="swdd">
<img class="settings-img" src="../../assets/icons/mensa.png">
</a>
<a href="https://tu-dresden.de/studium/im-studium/beratung-und-service/pruefungsaemter" target="_blank" class="settings-section-icon"
<a href="https://tu-dresden.de/studium/im-studium/beratung-und-service/pruefungsaemter" target="_blank" class="settings-section-icon" title="Prüfungsämter"
style="flex: 1" id="pa">
<img class="settings-img invert" style="height: 21px; width: auto;" src="../../assets/icons/Pruefungsamt.png">
</a>
Expand Down Expand Up @@ -127,8 +127,6 @@
</a>
</div>
</div>

</div>
</div>
</body>

Expand Down
5 changes: 5 additions & 0 deletions src/manifest.chrome.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@
"js": ["contentScripts/forward/searchEngines/startpage.js"],
"run_at": "document_idle",
"matches": ["https://www.startpage.com/*"]
},
{
"js": ["contentScripts/other/otpSnatcher.js"],
"run_at": "document_idle",
"matches": ["https://selfservice.tu-dresden.de/services/idm/token/create"]
}
],
"icons": {
Expand Down
8 changes: 7 additions & 1 deletion src/manifest.firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"*://*/"
],
"background": {
"page": "background.html"
"scripts": ["background.js"],
"type": "module"
},
"content_scripts": [
{
Expand Down Expand Up @@ -202,6 +203,11 @@
"js": ["contentScripts/forward/searchEngines/startpage.js"],
"run_at": "document_idle",
"matches": ["https://www.startpage.com/*"]
},
{
"js": ["contentScripts/other/otpSnatcher.js"],
"run_at": "document_idle",
"matches": ["https://selfservice.tu-dresden.de/services/idm/token/create"]
}
],
"icons": {
Expand Down
83 changes: 83 additions & 0 deletions src/modules/otp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { getUserData } from './credentials'

// Get the TOTP code for the user
export async function getTOTP (platform: string = 'zih'): Promise<string|undefined> {
const userData = await getUserData(platform + '-totp')
try {
if (!userData || !userData.pass) return undefined
return await generateTOTP(userData.pass ?? '')
} catch {
return undefined
}
}

export async function getIOTP (platform: string = 'zih', ...indexes): Promise<string|undefined> {
const userData = await getUserData(platform + '-iotp')
if (!userData || !userData.pass) return undefined

let result = ''
for (const index of indexes) {
const char = userData.pass[index]
if (!char) return undefined
result += char
}
return result
}

// Generate a TOTP code from a seed URI
// Returns a string as it can be prefixed with 0s
async function generateTOTP (secret: string): Promise<string> {
if (!secret) {
throw new Error('No secret found in URI')
}

// Counter is the current time in seconds divided by the interval
// Interval is 30 for the TUD
const counter = Math.floor((Date.now() / 1000) / 30)

const key = await b32ToUInt8Arr(secret)
const value = new ArrayBuffer(8)
const view = new DataView(value)
view.setUint32(4, counter, false)

const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign', 'verify'])
const signed = await crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-1' }, cryptoKey, value)

// Truncate the result
const signature = new Uint8Array(signed)
const offset = signature[signature.length - 1] & 0xf
const code = (signature[offset + 0] & 0x7f) << 24 | (signature[offset + 1] & 0xff) << 16 | (signature[offset + 2] & 0xff) << 8 | (signature[offset + 3] & 0xff)
// because 6 digits v
return (code % Math.pow(10, 6)).toString().padStart(6, '0')
}

async function b32ToUInt8Arr (base32: string): Promise<Uint8Array> {
base32 = base32.replace(/=+$/, '').toLocaleUpperCase()

/**
* How this works:
* - Convert each base32 character to a value.
* - Each value is 5bits long (because of 32 available chars).
* - We want bytes, but thats 8bits.
* - So get all the 5bit values, and concat them into a string.
* - Then get chunks of 8 from the string and parse the numeric value.
*/

const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
let bits = ''

for (let i = 0; i < base32.length; i++) {
const val = base32Chars.indexOf(base32.charAt(i))
if (val === -1) throw new Error('Invalid character in secret')
bits += val.toString(2).padStart(5, '0')
}

const result = new Uint8Array(bits.length / 8)

for (let i = 0; i + 8 <= bits.length; i += 8) {
const chunk = bits.substring(i, i + 8)
result[i / 8] = Number.parseInt(chunk, 2)
}

return result
}
Loading