From ca6746a0bdfe9cf125a1c281d9e40e8bc0af5f11 Mon Sep 17 00:00:00 2001
From: Kunal Mehta <legoktm@debian.org>
Date: Mon, 21 Feb 2022 21:55:03 -0800
Subject: [PATCH] [WIP] Encrypt OTP secret in the database

Needs tests, migration script, etc.

Bug: T145915
Change-Id: I0aaeb17ed44470296392d5d9bdc46a5bcdc72b0c
---
 extension.json               |  5 ++-
 src/Key/EncryptionHelper.php | 75 ++++++++++++++++++++++++++++++++++++
 src/Key/TOTPKey.php          |  8 +++-
 3 files changed, 86 insertions(+), 2 deletions(-)
 create mode 100644 src/Key/EncryptionHelper.php

diff --git a/extension.json b/extension.json
index 0828fe3..0423e2f 100644
--- a/extension.json
+++ b/extension.json
@@ -10,7 +10,10 @@
 	"descriptionmsg": "oathauth-desc",
 	"type": "other",
 	"requires": {
-		"MediaWiki": ">= 1.37.0"
+		"MediaWiki": ">= 1.37.0",
+		"platform": {
+			"ext-sodium": "*"
+		}
 	},
 	"license-name": "GPL-2.0-or-later AND GPL-3.0-or-later",
 	"attributes": {
diff --git a/src/Key/EncryptionHelper.php b/src/Key/EncryptionHelper.php
new file mode 100644
index 0000000..695eb30
--- /dev/null
+++ b/src/Key/EncryptionHelper.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Copyright (C) 2022 Kunal Mehta <legoktm@debian.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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+namespace MediaWiki\Extension\OATHAuth\Key;
+
+use Base32\Base32;
+
+/**
+ * Wrapper around sodium's cryptobox to encrypt and decrypt the OTP secret
+ */
+class EncryptionHelper {
+
+	/**
+	 * Get the encryption secret key as bytes
+	 * 
+	 * @return string
+	 */
+	private static function getKey() {
+		global $wgOATHSecretKey, $wgSecretKey;
+		return hex2bin( $wgOATHSecretKey ?? $wgSecretKey );
+	}
+
+	/**
+	 * Decrypt the given ciphertext
+	 * 
+	 * @param string $ciphertext base32 encoded
+	 * @param string $nonce base32 encoded
+	 * @return string
+	 * @throws \Exception When decryption fails
+	 */
+	public static function decrypt( string $ciphertext, string $nonce ) {
+		$plaintext = sodium_crypto_secretbox_open(
+			Base32::decode( $ciphertext ),
+			Base32::decode( $nonce ),
+			self::getKey()
+		);
+		if ( $plaintext === false ) {
+			throw new \Exception( 'Unable to decrypt ciphertext' );
+		}
+		return $plaintext;
+	}
+
+	/**
+	 * Encrypt the given plaintext
+	 * 
+	 * @param string $plaintext What to encrypt
+	 * @return string[] Array with 'ciphertext' and 'nonce' keys, both base32 encoded
+	 */
+	public static function encrypt( string $plaintext ) {
+		// Generate a unique nonce
+		$nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
+		$ciphertext = sodium_crypto_secretbox( $plaintext, $nonce, self::getKey() );
+		return [
+			'ciphertext' => Base32::encode( $ciphertext ),
+			'nonce' => Base32::encode( $nonce ),
+		];
+	}
+}
diff --git a/src/Key/TOTPKey.php b/src/Key/TOTPKey.php
index ffbe77a..ce940e8 100644
--- a/src/Key/TOTPKey.php
+++ b/src/Key/TOTPKey.php
@@ -94,6 +94,10 @@ class TOTPKey implements IAuthKey {
 		if ( !isset( $data['secret'] ) || !isset( $data['scratch_tokens'] ) ) {
 			return null;
 		}
+		if ( isset( $data['nonce'] ) ) {
+			$data['secret'] = EncryptionHelper::decrypt( $data['secret'], $data['nonce'] );
+			unset( $data['nonce'] );
+		}
 		return new static( $data['secret'], $data['scratch_tokens'] );
 	}
 
@@ -256,8 +260,10 @@ class TOTPKey implements IAuthKey {
 	}
 
 	public function jsonSerialize() {
+		$encrypted = EncryptionHelper::encrypt( $this->getSecret() );
 		return [
-			'secret' => $this->getSecret(),
+			'secret' => $encrypted['ciphertext'],
+			'nonce' => $encrypted['nonce'],
 			'scratch_tokens' => $this->getScratchTokens()
 		];
 	}
-- 
2.34.1

