feat: add 2fa support (#525)

This commit is contained in:
Miguel Ribeiro
2024-09-28 18:33:09 +02:00
committed by GitHub
parent 47f50ec952
commit 2f16ab3fdf
72 changed files with 4875 additions and 501 deletions

View File

@@ -69,6 +69,15 @@ require_once 'includes/header.php';
</a>
</span>
</p>
<p>
QRCode.js:
<span>
https://github.com/davidshimjs/qrcodejs
<a href="https://github.com/davidshimjs/qrcodejs" target="_blank" title="<?= translate('external_url', $i18n) ?>">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
</a>
</span>
</p>
</div>
</section>

View File

@@ -0,0 +1,123 @@
<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/inputvalidation.php';
if (!function_exists('trigger_deprecation')) {
function trigger_deprecation($package, $version, $message, ...$args)
{
if (PHP_VERSION_ID >= 80000) {
trigger_error(sprintf($message, ...$args), E_USER_DEPRECATED);
}
}
}
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
die(json_encode([
"success" => false,
"message" => translate('session_expired', $i18n),
"reload" => false
]));
}
$statement = $db->prepare('SELECT totp_enabled FROM user WHERE id = :id');
$statement->bindValue(':id', $userId, SQLITE3_INTEGER);
$result = $statement->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row['totp_enabled'] == 0) {
die(json_encode([
"success" => false,
"message" => "2FA is not enabled for this user",
"reload" => true
]));
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$postData = file_get_contents("php://input");
$data = json_decode($postData, true);
if (isset($data['totpCode']) && $data['totpCode'] != "") {
require_once __DIR__ . '/../../libs/OTPHP/FactoryInterface.php';
require_once __DIR__ . '/../../libs/OTPHP/Factory.php';
require_once __DIR__ . '/../../libs/OTPHP/ParameterTrait.php';
require_once __DIR__ . '/../../libs/OTPHP/OTPInterface.php';
require_once __DIR__ . '/../../libs/OTPHP/OTP.php';
require_once __DIR__ . '/../../libs/OTPHP/TOTPInterface.php';
require_once __DIR__ . '/../../libs/OTPHP/TOTP.php';
require_once __DIR__ . '/../../libs/Psr/Clock/ClockInterface.php';
require_once __DIR__ . '/../../libs/OTPHP/InternalClock.php';
require_once __DIR__ . '/../../libs/constant_time_encoding/Binary.php';
require_once __DIR__ . '/../../libs/constant_time_encoding/EncoderInterface.php';
require_once __DIR__ . '/../../libs/constant_time_encoding/Base32.php';
$totp_code = $data['totpCode'];
$statement = $db->prepare('SELECT totp_secret FROM totp WHERE user_id = :id');
$statement->bindValue(':id', $userId, SQLITE3_INTEGER);
$result = $statement->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$secret = $row['totp_secret'];
$statement = $db->prepare('SELECT backup_codes FROM totp WHERE user_id = :id');
$statement->bindValue(':id', $userId, SQLITE3_INTEGER);
$result = $statement->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$backupCodes = $row['backup_codes'];
$clock = new OTPHP\InternalClock();
$totp = OTPHP\TOTP::createFromSecret($secret, $clock);
if ($totp->verify($totp_code)) {
$statement = $db->prepare('UPDATE user SET totp_enabled = 0 WHERE id = :id');
$statement->bindValue(':id', $userId, SQLITE3_INTEGER);
$statement->execute();
$statement = $db->prepare('DELETE FROM totp WHERE user_id = :id');
$statement->bindValue(':id', $userId, SQLITE3_INTEGER);
$statement->execute();
die(json_encode([
"success" => true,
"message" => translate('success', $i18n),
"reload" => true
]));
} else {
// Compare the TOTP code agains the backup codes
$backupCodes = json_decode($backupCodes, true);
if (($key = array_search($totp_code, $backupCodes)) !== false) {
unset($backupCodes[$key]);
$statement = $db->prepare('UPDATE totp SET backup_codes = :backup_codes WHERE user_id = :id');
$statement->bindValue(':id', $userId, SQLITE3_INTEGER);
$statement->bindValue(':backup_codes', json_encode($backupCodes), SQLITE3_TEXT);
$statement->execute();
die(json_encode([
"success" => true,
"message" => translate('success', $i18n),
"reload" => true
]));
} else {
die(json_encode([
"success" => false,
"message" => translate('totp_code_incorrect', $i18n),
"reload" => false
]));
}
}
} else {
die(json_encode([
"success" => false,
"message" => translate('fields_missing', $i18n),
"reload" => false
]));
}
} else {
die(json_encode([
"success" => false,
"message" => translate('invalid_request_method', $i18n),
"reload" => false
]));
}

View File

@@ -0,0 +1,139 @@
<?php
require_once '../../includes/connect_endpoint.php';
require_once '../../includes/inputvalidation.php';
if (!function_exists('trigger_deprecation')) {
function trigger_deprecation($package, $version, $message, ...$args)
{
if (PHP_VERSION_ID >= 80000) {
trigger_error(sprintf($message, ...$args), E_USER_DEPRECATED);
}
}
}
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
die(json_encode([
"success" => false,
"message" => translate('session_expired', $i18n)
]));
}
if ($_SERVER["REQUEST_METHOD"] === "GET") {
function base32_encode($hex)
{
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
$bin = '';
foreach (str_split($hex) as $char) {
$bin .= str_pad(base_convert($char, 16, 2), 4, '0', STR_PAD_LEFT);
}
$chunks = str_split($bin, 5);
$base32 = '';
foreach ($chunks as $chunk) {
$chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT);
$index = bindec($chunk);
$base32 .= $alphabet[$index];
}
return $base32;
}
$data = $_GET;
if (isset($data['generate']) && $data['generate'] == true) {
$secret = base32_encode(bin2hex(random_bytes(20)));
$qrCodeUrl = "otpauth://totp/Wallos:" . $_SESSION['username'] . "?secret=" . $secret . "&issuer=Wallos";
$response = [
"success" => true,
"secret" => $secret,
"qrCodeUrl" => $qrCodeUrl
];
echo json_encode($response);
}
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$postData = file_get_contents("php://input");
$data = json_decode($postData, true);
if (isset($data['totpSecret']) && $data['totpSecret'] != "" && isset($data['totpCode']) && $data['totpCode'] != "") {
require_once __DIR__ . '/../../libs/OTPHP/FactoryInterface.php';
require_once __DIR__ . '/../../libs/OTPHP/Factory.php';
require_once __DIR__ . '/../../libs/OTPHP/ParameterTrait.php';
require_once __DIR__ . '/../../libs/OTPHP/OTPInterface.php';
require_once __DIR__ . '/../../libs/OTPHP/OTP.php';
require_once __DIR__ . '/../../libs/OTPHP/TOTPInterface.php';
require_once __DIR__ . '/../../libs/OTPHP/TOTP.php';
require_once __DIR__ . '/../../libs/Psr/Clock/ClockInterface.php';
require_once __DIR__ . '/../../libs/OTPHP/InternalClock.php';
require_once __DIR__ . '/../../libs/constant_time_encoding/Binary.php';
require_once __DIR__ . '/../../libs/constant_time_encoding/EncoderInterface.php';
require_once __DIR__ . '/../../libs/constant_time_encoding/Base32.php';
$secret = $data['totpSecret'];
$totp_code = $data['totpCode'];
// Check if user already has TOTP enabled
$stmt = $db->prepare("SELECT totp_enabled FROM user WHERE id = :user_id");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row['totp_enabled'] == 1) {
die(json_encode([
"success" => false,
"message" => translate('2fa_already_enabled', $i18n)
]));
}
$clock = new OTPHP\InternalClock();
$totp = OTPHP\TOTP::createFromSecret($secret, $clock);
if ($totp->verify($totp_code)) {
// Generate 10 backup codes
$backupCodes = [];
for ($i = 0; $i < 10; $i++) {
$backupCode = bin2hex(random_bytes(10));
$backupCodes[] = $backupCode;
}
// Remove old TOTP data
$stmt = $db->prepare("DELETE FROM totp WHERE user_id = :user_id");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
$stmt = $db->prepare("INSERT INTO totp (user_id, totp_secret, backup_codes, last_totp_used) VALUES (:user_id, :totp_secret, :backup_codes, :last_totp_used)");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->bindValue(':totp_secret', $secret, SQLITE3_TEXT);
$stmt->bindValue(':backup_codes', json_encode($backupCodes), SQLITE3_TEXT);
$stmt->bindValue(':last_totp_used', time(), SQLITE3_INTEGER);
$stmt->execute();
// Update user totp_enabled
$stmt = $db->prepare("UPDATE user SET totp_enabled = 1 WHERE id = :user_id");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
die(json_encode([
"success" => true,
"backupCodes" => $backupCodes,
"message" => translate('success', $i18n)
]));
} else {
die(json_encode([
"success" => false,
"message" => translate('totp_code_incorrect', $i18n)
]));
}
} else {
die(json_encode([
"success" => false,
"message" => translate('totp_code_incorrect', $i18n)
]));
}
}

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Avatar hochladen",
'file_type_error' => "Dateityp nicht unterstützt",
'user_details' => "Benutzerdetails",
'two_factor_authentication' => "Zwei-Faktor-Authentifizierung",
'two_factor_info' => "Die Zwei-Faktor-Authentifizierung fügt Ihrem Konto eine zusätzliche Sicherheitsebene hinzu.<br>Sie benötigen eine Authentifizierungs-App wie Google Authenticator, Authy oder Ente Auth, um den QR-Code zu scannen.",
"two_factor_enabled_info" => "Ihr Konto ist mit der Zwei-Faktor-Authentifizierung gesichert. Sie können sie deaktivieren, indem Sie auf die Schaltfläche oben klicken.",
"enable_two_factor_authentication" => "Zwei-Faktor-Authentifizierung aktivieren",
"2fa_already_enabled" => "Zwei-Faktor-Authentifizierung ist bereits aktiviert",
"totp_code_incorrect" => "TOTP-Code ist falsch",
"backup_codes" => "Backup-Codes",
"download_backup_codes" => "Backup-Codes herunterladen",
"copy_to_clipboard" => "In die Zwischenablage kopieren",
"totp_backup_codes_info" => "Speichern Sie diese Codes an einem sicheren Ort. Sie können sie verwenden, wenn Sie keinen Zugriff auf Ihre Authentifizierungs-App haben.",
"disable_two_factor_authentication" => "Zwei-Faktor-Authentifizierung deaktivieren",
"totp_code" => "TOTP-Code",
"monthly_budget" => "Monatliches Budget",
"budget_info" => "Das monatliche Budget wird für die Berechnung der Statistiken verwendet.",
"household" => "Haushalt",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Oktober",
"month-11" => "November",
"month-12" => "Dezember",
// TOTP Page
"insert_totp_code" => "Bitte geben Sie den TOTP-Code ein",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "μεταφόρτωση άβαταρ",
'file_type_error' => "Το αρχείο πρέπει να είναι τύπου jpg, jpeg, png, webp ή gif",
'user_details' => "Λεπτομέρειες χρήστη",
'two_factor_authentication' => "Διπλής πιστοποίησης",
"two_factor_info" => "Ο έλεγχος ταυτότητας δύο παραγόντων προσθέτει ένα επιπλέον επίπεδο ασφάλειας στο λογαριασμό σας.<br>Θα χρειαστείτε μια εφαρμογή ελέγχου ταυτότητας όπως το Google Authenticator, το Authy ή το Ente Auth για να σαρώσετε τον κωδικό QR.",
"two_factor_enabled_info" => "Ο λογαριασμός σας είναι ασφαλής με τον έλεγχο ταυτότητας δύο παραγόντων. Μπορείτε να τον απενεργοποιήσετε κάνοντας κλικ στο κουμπί παραπάνω.",
"enable_two_factor_authentication" => "Ενεργοποίηση διπλής πιστοποίησης",
"2fa_already_enabled" => "Ο έλεγχος ταυτότητας δύο παραγόντων είναι ήδη ενεργοποιημένος",
"totp_code_incorrect" => "Ο κωδικός TOTP είναι εσφαλμένος",
"backup_codes" => "Κωδικοί ανάκτησης",
"download_backup_codes" => "Κατέβασε τους κωδικούς ανάκτησης",
"copy_to_clipboard" => "Αντιγραφή στο πρόχειρο",
"totp_backup_codes_info" => "Αποθηκεύστε αυτούς τους κωδικούς ανάκτησης σε ένα ασφαλές μέρος. Θα χρειαστείτε έναν από αυτούς τους κωδικούς ανάκτησης για να αποκτήσετε πρόσβαση στο λογαριασμό σας σε περίπτωση που χάσετε τη συσκευή σας.",
"disable_two_factor_authentication" => "Απενεργοποίηση διπλής πιστοποίησης",
"totp_code" => "Κωδικός TOTP",
"monthly_budget" => "Μηνιαίος προϋπολογισμός",
"budget_info" => "Ο μηνιαίος προϋπολογισμός χρησιμοποιείται για τον υπολογισμό των στατιστικών",
"household" => "Νοικοκυριό",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Οκτώβριος",
"month-11" => "Νοέμβριος",
"month-12" => "Δεκέμβριος",
// TOTP Page
"insert_totp_code" => "Εισάγετε τον κωδικό TOTP",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Upload Avatar",
'file_type_error' => "The file type supplied is not supported.",
'user_details' => "User Details",
'two_factor_authentication' => "Two Factor Authentication",
'two_factor_info' => "Two Factor Authentication adds an extra layer of security to your account.<br>You will need an authenticator app like Google Authenticator, Authy or Ente Auth to scan the QR code.",
"two_factor_enabled_info" => "Your account is secure with Two Factor Authentication. You can disable it by clicking the button above.",
"enable_two_factor_authentication" => "Enable Two Factor Authentication",
"2fa_already_enabled" => "Two Factor Authentication is already enabled",
"totp_code_incorrect" => "TOTP code is incorrect",
"backup_codes" => "Backup Codes",
"download_backup_codes" => "Download Backup Codes",
"copy_to_clipboard" => "Copy to clipboard",
"totp_backup_codes_info" => "These codes can be used to login if you lose access to your authenticator app.",
"disable_two_factor_authentication" => "Disable Two Factor Authentication",
"totp_code" => "TOTP Code",
"monthly_budget" => "Monthly Budget",
"budget_info" => "Monthly budget is used to calculate statistics",
"household" => "Household",
@@ -345,6 +357,8 @@ $i18n = [
"month-10" => "October",
"month-11" => "November",
"month-12" => "December",
// TOTP Page
"insert_totp_code" => "Insert TOTP code",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Subir avatar",
'file_type_error' => "El archivo debe ser una imagen en formato PNG, JPG, WEBP o SVG",
'user_details' => "Detalles del Usuario",
'two_factor_authentication' => "Autenticación de Dos Factores",
'two_factor_info' => "La autenticación de dos factores añade una capa adicional de seguridad a tu cuenta.<br>Necesitarás una aplicación de autenticación como Google Authenticator, Authy o Ente Auth para escanear el código QR.",
'two_factor_enabled_info' => "Tu cuenta está segura con la autenticación de dos factores. Puedes desactivarla haciendo clic en el botón de arriba.",
"enable_two_factor_authentication" => "Habilitar Autenticación de Dos Factores",
"2fa_already_enabled" => "La autenticación de dos factores ya está habilitada",
"totp_code_incorrect" => "El código TOTP es incorrecto",
"backup_codes" => "Códigos de Respaldo",
"download_backup_codes" => "Descargar Códigos de Respaldo",
"copy_to_clipboard" => "Copiar al Portapapeles",
"totp_backup_codes_info" => "Guarda estos códigos en un lugar seguro. Puedes usarlos si pierdes acceso a tu aplicación de autenticación.",
"disable_two_factor_authentication" => "Desactivar Autenticación de Dos Factores",
"totp_code" => "Código TOTP",
"monthly_budget" => "Presupuesto Mensual",
"budget_info" => "El presupuesto mensual se utiliza para calcular las estadísticas. Si no deseas utilizar esta función, déjalo en 0.",
"household" => "Hogar",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Octubre",
"month-11" => "Noviembre",
"month-12" => "Diciembre",
// TOTP Page
"insert_totp_code" => "Introduce el código TOTP",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Télécharger un Avatar",
'file_type_error' => "Le type de fichier n'est pas pris en charge",
'user_details' => "Détails de l'utilisateur",
'two_factor_authentication' => "Authentification à deux facteurs",
'two_factor_info' => "L'authentification à deux facteurs ajoute une couche supplémentaire de sécurité à votre compte. <br>Vous aurez besoin d'une application d'authentification comme Google Authenticator, Authy ou Ente Auth pour scanner le code QR.",
'two_factor_enabled_info' => "Votre compte est sécurisé grâce à l'authentification à deux facteurs. Vous pouvez la désactiver en cliquant sur le bouton ci-dessus.",
"enable_two_factor_authentication" => "Activer l'authentification à deux facteurs",
"2fa_already_enabled" => "L'authentification à deux facteurs est déjà activée",
"totp_code_incorrect" => "Le code TOTP est incorrect",
"backup_codes" => "Codes de sauvegarde",
"download_backup_codes" => "Télécharger les codes de sauvegarde",
"copy_to_clipboard" => "Copier dans le presse-papiers",
"totp_backup_codes_info" => "Conservez ces codes en lieu sûr. Vous ne pourrez pas les récupérer plus tard.",
"disable_two_factor_authentication" => "Désactiver l'authentification à deux facteurs",
"totp_code" => "Code TOTP",
"monthly_budget" => "Budget mensuel",
"budget_info" => "Le budget mensuel est utilisé pour calculer les statistiques. Laissez vide pour désactiver.",
"household" => "Ménage",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Octobre",
"month-11" => "Novembre",
"month-12" => "Décembre",
// TOTP Page
"insert_totp_code" => "Veuillez insérer le code TOTP",
];

View File

@@ -128,6 +128,18 @@ $i18n = [
'upload_avatar' => 'Carica avatar',
'file_type_error' => 'Il tipo di file fornito non è supportato.',
'user_details' => 'Dettagli utente',
'two_factor_authentication' => 'Autenticazione a due fattori',
'two_factor_info' => "L'Autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al vostro account.<br>Per scansionare il codice QR è necessaria un'app di autenticazione come Google Authenticator, Authy o Ente Auth.",
'two_factor_enabled_info' => "Il vostro account è sicuro con l'Autenticazione a due fattori. È possibile disattivarla facendo clic sul pulsante in alto.",
"enable_two_factor_authentication" => "Abilita l'autenticazione a due fattori",
"2fa_already_enabled" => "L'autenticazione a due fattori è già abilitata",
"totp_code_incorrect" => "Il codice TOTP è incorretto",
"backup_codes" => "Codici di backup",
"download_backup_codes" => "Scarica i codici di backup",
"copy_to_clipboard" => "Copia negli appunti",
"totp_backup_codes_info" => "I codici di backup possono essere utilizzati per accedere al tuo account se non hai accesso al tuo dispositivo di autenticazione a due fattori.",
"disable_two_factor_authentication" => "Disabilita l'autenticazione a due fattori",
"totp_code" => "Codice TOTP",
"monthly_budget" => "Budget mensile",
"budget_info" => "Il budget mensile viene utilizzato per calcolare le statistiche. Se non si desidera utilizzare questa funzionalità, impostare il budget su 0.",
'household' => 'Nucleo familiare',
@@ -364,6 +376,9 @@ $i18n = [
"month-10" => "Ottobre",
"month-11" => "Novembre",
"month-12" => "Dicembre",
// TOTP Page
"insert_totp_code" => "Inserisci il codice TOTP",
];
?>

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "アバターをアップロードする",
'file_type_error' => "ファイルタイプが許可されていません",
'user_details' => "ユーザー詳細",
'two_factor_authentication' => "2要素認証",
'two_factor_info' => "二要素認証は、アカウントに追加のセキュリティレイヤーを追加します。QR コードをスキャンするには、Google Authenticator、Authy、Ente Auth などの認証アプリが必要です。",
'two_factor_enabled_info' => "お客様のアカウントは二要素認証で保護されています。上のボタンをクリックして無効にすることができます。",
"enable_two_factor_authentication" => "二要素認証を有効にする",
"2fa_already_enabled" => "2要素認証は既に有効です",
"totp_code_incorrect" => "TOTPコードが正しくありません",
"backup_codes" => "バックアップコード",
"download_backup_codes" => "バックアップコードをダウンロード",
"copy_to_clipboard" => "クリップボードにコピー",
"totp_backup_codes_info" => "これらのコードは、2要素認証アプリが利用できない場合に使用します。コードは一度しか表示されません。",
"disable_two_factor_authentication" => "二要素認証を無効にする",
"totp_code" => "TOTPコード",
"monthly_budget" => "月間予算",
"budget_info" => "予算を設定すると、統計ページで予算と実際の支出を比較できます。",
"household" => "世帯",
@@ -337,6 +349,8 @@ $i18n = [
"month-10" => "10月",
"month-11" => "11月",
"month-12" => "12月",
// TOTP Page
"insert_totp_code" => "TOTPコードを入力してください",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "아바타 업로드",
'file_type_error' => "제공된 파일이 지원하지 않는 타입입니다.",
'user_details' => "유저 상세",
'two_factor_authentication' => "이중 인증",
'two_factor_info' => "2단계 인증은 계정에 보안을 한층 더 강화합니다. QR 코드를 스캔하려면 Google Authenticator, Authy 또는 Ente Auth와 같은 인증 앱이 필요합니다.",
'two_factor_enabled_info' => "계정은 2단계 인증으로 안전하게 보호됩니다. 위의 버튼을 클릭하여 비활성화할 수 있습니다.",
"enable_two_factor_authentication" => "2단계 인증 활성화",
"2fa_already_enabled" => "2단계 인증이 이미 활성화되어 있습니다.",
"totp_code_incorrect" => "TOTP 코드가 올바르지 않습니다.",
"backup_codes" => "백업 코드",
"download_backup_codes" => "백업 코드 다운로드",
"copy_to_clipboard" => "클립보드로 복사",
"totp_backup_codes_info" => "이 코드는 계정에 대한 백업 코드입니다. 이 코드를 안전한 곳에 보관하세요. 이 코드는 한 번만 사용할 수 있습니다.",
"disable_two_factor_authentication" => "2단계 인증 비활성화",
"totp_code" => "TOTP 코드",
"monthly_budget" => "월간 예산",
"budget_info" => "예산을 설정하면 통계 페이지에서 예산과 실제 지출을 비교할 수 있습니다.",
"household" => "가구",
@@ -345,6 +357,8 @@ $i18n = [
"month-10" => "10월",
"month-11" => "11월",
"month-12" => "12월",
// TOTP Page
"insert_totp_code" => "2단계 인증 코드를 입력하세요",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Prześlij awatar",
'file_type_error' => "Podany typ pliku nie jest obsługiwany.",
'user_details' => "Szczegóły użytkownika",
'two_factor_authentication' => "Uwierzytelnianie dwuskładnikowe",
'two_factor_info' => "Uwierzytelnianie dwuskładnikowe dodaje dodatkową warstwę zabezpieczeń do konta.<br>Do zeskanowania kodu QR potrzebna będzie aplikacja uwierzytelniająca, taka jak Google Authenticator, Authy lub Ente Auth.",
'two_factor_enabled_info' => "Twoje konto jest bezpieczne dzięki uwierzytelnianiu dwuetapowemu. Możesz ją wyłączyć, klikając przycisk powyżej.",
"enable_two_factor_authentication" => "Włącz uwierzytelnianie dwuskładnikowe",
"2fa_already_enabled" => "Uwierzytelnianie dwuskładnikowe jest już włączone",
"totp_code_incorrect" => "Kod TOTP jest nieprawidłowy",
"backup_codes" => "Kody zapasowe",
"download_backup_codes" => "Pobierz kody zapasowe",
"copy_to_clipboard" => "Skopiuj do schowka",
"totp_backup_codes_info" => "Kody zapasowe są jednorazowe i można je użyć do zalogowania się, jeśli nie masz dostępu do aplikacji uwierzytelniającej.",
"disable_two_factor_authentication" => "Wyłącz uwierzytelnianie dwuskładnikowe",
"totp_code" => "Kod TOTP",
"monthly_budget" => "Miesięczny budżet",
"budget_info" => "Jeśli ustawisz budżet, zobaczysz pasek postępu na stronie głównej.",
"household" => "Gospodarstwo domowe",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Październik",
"month-11" => "Listopad",
"month-12" => "Grudzień",
// TOTP Page
"insert_totp_code" => "Wprowadź kod TOTP",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Enviar avatar",
'file_type_error' => "Tipo de ficheiro não permitido",
'user_details' => "Detalhes do utilizador",
'two_factor_authentication' => "Autenticação de dois fatores",
'two_factor_info' => "A autenticação de dois factores acrescenta uma camada extra de segurança à sua conta.<br>Necessitará de uma aplicação de autenticação como o Google Authenticator, Authy ou Ente Auth para ler o código QR.",
'two_factor_enabled_info' => "A sua conta está segura com a autenticação de dois factores. Pode desactivá-la clicando no botão acima.",
"enable_two_factor_authentication" => "Activar autenticação de dois factores",
"2fa_already_enabled" => "A autenticação de dois factores já está activada",
"totp_code_incorrect" => "Código TOTP incorrecto",
"backup_codes" => "Códigos de Backup",
"download_backup_codes" => "Descarregar códigos de backup",
"copy_to_clipboard" => "Copiar para a área de transferência",
"totp_backup_codes_info" => "Guarde estes códigos num local seguro. Pode usá-los para aceder à sua conta se perder o acesso ao seu dispositivo de autenticação.",
"disable_two_factor_authentication" => "Desactivar autenticação de dois factores",
"totp_code" => "Código TOTP",
"monthly_budget" => "Orçamento Mensal",
"budget_info" => "Ao definir um orçamento pode comparar com os gastos reais na página de estatísticas.",
"household" => "Agregado",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Outubro",
"month-11" => "Novembro",
"month-12" => "Dezembro",
// TOTP Page
"insert_totp_code" => "Insira o código TOTP",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Carregar avatar",
'file_type_error' => "Tipo de arquivo não permitido",
'user_details' => "Informações do Usuário",
'two_factor_authentication' => "Autenticação de dois fatores",
'two_factor_info' => "A autenticação de dois fatores adiciona uma camada extra de segurança à sua conta.<br>Você precisará de um aplicativo autenticador, como o Google Authenticator, Authy ou Ente Auth, para ler o código QR.",
'two_factor_enabled_info' => "Sua conta está segura com a autenticação de dois fatores. Você pode desativá-la clicando no botão acima.",
"enable_two_factor_authentication" => "Ativar autenticação de dois fatores",
"2fa_already_enabled" => "A autenticação de dois fatores já está ativada",
"totp_code_incorrect" => "Código TOTP incorreto",
"backup_codes" => "Códigos de backup",
"download_backup_codes" => "Baixar códigos de backup",
"copy_to_clipboard" => "Copiar para a área de transferência",
"totp_backup_codes_info" => "Guarde esses códigos em um lugar seguro. Eles podem ser usados para acessar sua conta se você perder o acesso ao aplicativo de autenticação.",
"disable_two_factor_authentication" => "Desativar autenticação de dois fatores",
"totp_code" => "Código TOTP",
"monthly_budget" => "Orçamento mensal",
"budget_info" => "O orçamento mensal é usado para calcular estatísticas",
"household" => "Membros",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Outubro",
"month-11" => "Novembro",
"month-12" => "Dezembro",
// TOTP Page
"insert_totp_code" => "Insira o código TOTP",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Загрузить аватар",
'file_type_error' => "Указанный тип файла не поддерживается.",
'user_details' => "Данные пользователя",
'two_factor_authentication' => "Двухфакторная аутентификация",
'two_factor_info' => "Двухфакторная аутентификация добавляет дополнительный уровень безопасности к вашей учетной записи.<br>Для сканирования QR-кода вам понадобится приложение-аутентификатор, например Google Authenticator, Authy или Ente Auth.",
'two_factor_enabled_info' => "Ваш аккаунт защищен с помощью двухфакторной аутентификации. Вы можете отключить ее, нажав на кнопку выше.",
"enable_two_factor_authentication" => "Включить двухфакторную аутентификацию",
"2fa_already_enabled" => "Двухфакторная аутентификация уже включена",
"totp_code_incorrect" => "Код TOTP неверен",
"backup_codes" => "Резервные коды",
"download_backup_codes" => "Скачать резервные коды",
"copy_to_clipboard" => "Скопировать в буфер обмена",
"totp_backup_codes_info" => "Сохраните эти коды в безопасном месте. Они могут быть использованы для входа в систему, если вы потеряете доступ к приложению аутентификации.",
"disable_two_factor_authentication" => "Отключить двухфакторную аутентификацию",
"totp_code" => "Код TOTP",
"monthly_budget" => "Ежемесячный бюджет",
"budget_info" => "Если вы укажете бюджет, Wallos будет отображать вашу текущую стоимость подписок в сравнении с вашим бюджетом.",
"household" => "Семья",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Октябрь",
"month-11" => "Ноябрь",
"month-12" => "Декабрь",
// TOTP Page
"insert_totp_code" => "Введите код TOTP",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Naloži avatar",
'file_type_error' => "Vrsta datoteke ni podprta.",
'user_details' => "Podrobnosti o uporabniku",
'two_factor_authentication' => "Dvojna preverba pristnosti",
'two_factor_info' => "Two Factor Authentication adds an extra layer of security to your account.<br>Za optično branje kode QR potrebujete aplikacijo za preverjanje pristnosti, kot so Google Authenticator, Authy ali Ente Auth.",
'two_factor_enabled_info' => "Vaš račun je varen z dvostopenjskim preverjanjem pristnosti. Onemogočite jo lahko tako, da kliknete zgornji gumb.",
"enable_two_factor_authentication" => "Omogoči dvostopenjsko preverjanje pristnosti",
"2fa_already_enabled" => "Dvostopenjsko preverjanje pristnosti je že omogočeno",
"totp_code_incorrect" => "Koda TOTP je napačna",
"backup_codes" => "Rezervne kode",
"download_backup_codes" => "Prenesi rezervne kode",
"copy_to_clipboard" => "Kopiraj v odložišče",
"totp_backup_codes_info" => "Shranite te rezervne kode na varno mesto. Uporabite jih lahko, če izgubite dostop do svoje aplikacije za preverjanje pristnosti.",
"disable_two_factor_authentication" => "Onemogoči dvostopenjsko preverjanje pristnosti",
"totp_code" => "TOTP koda",
"monthly_budget" => "Mesečni proračun",
"budget_info" => "Mesečni proračun se uporablja za izračun statistike",
"household" => "Gospodinjstvo",
@@ -337,6 +349,8 @@ $i18n = [
"month-10" => "Oktober",
"month-11" => "November",
"month-12" => "December",
// TOTP Page
"insert_totp_code" => "Vnesite kodo TOTP",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Постави аватар",
'file_type_error' => "Датотека није у подржаном формату.",
'user_details' => "Кориснички детаљи",
'two_factor_authentication' => "Двофакторска аутентикација",
'two_factor_info' => "Двофакторска аутентификација додаје додатни ниво сигурности вашем налогу. <бр>Биће вам потребна апликација за аутентификацију као што је Гоогле Аутхентицатор, Аутхи или Енте Аутх да бисте скенирали КР код.",
'two_factor_enabled_info' => "Ваш налог је сигуран са двофакторском аутентификацијом. Можете га онемогућити кликом на дугме изнад.",
"enable_two_factor_authentication" => "Омогући двофакторску аутентикацију",
"2fa_already_enabled" => "Двофакторска аутентикација је већ омогућена",
"totp_code_incorrect" => "ТОТП код није исправан",
"backup_codes" => "Резервни кодови",
"download_backup_codes" => "Преузми резервне кодове",
"copy_to_clipboard" => "Копирај у клипборд",
"totp_backup_codes_info" => "Сачувајте ове кодове на безбедно место. Користићете их када изгубите приступ апликацији за аутентификацију.",
"disable_two_factor_authentication" => "Онемогући двофакторску аутентикацију",
"totp_code" => "ТОТП код",
"monthly_budget" => "Месечни буџет",
"budget_info" => "Унесите месечни буџет да бисте видели препоручену максималну цену претплате на почетној страници.",
"household" => "Домаћинство",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Октобар",
"month-11" => "Новембар",
"month-12" => "Децембар",
// TOTP Page
"insert_totp_code" => "Унесите ТОТП код",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Učitaj avatar",
'file_type_error' => "Tip datoteke koji ste priložili nije podržan.",
'user_details' => "Detalji korisnika",
'two_factor_authentication' => "Dvostruka autentifikacija",
'two_factor_info' => "Dvofaktorska autentifikacija dodaje dodatni nivo sigurnosti vašem nalogu. <br>Biće vam potrebna aplikacija za autentifikaciju kao što je Google Authenticator, Authi ili Ente Auth da biste skenirali KR kod.",
'two_factor_enabled_info' => "Vaš nalog je siguran sa dvofaktorskom autentifikacijom. Možete ga onemogućiti klikom na dugme iznad.",
"enable_two_factor_authentication" => "Omogući dvofaktorsku autentifikaciju",
"2fa_already_enabled" => "Dvofaktorska autentifikacija je već omogućena",
"totp_code_incorrect" => "Kod za dvofaktorsku autentifikaciju nije tačan",
"backup_codes" => "Rezervni kodovi",
"download_backup_codes" => "Preuzmi rezervne kodove",
"copy_to_clipboard" => "Kopiraj u clipboard",
"totp_backup_codes_info" => "Ovo su vaši rezervni kodovi za dvofaktorsku autentifikaciju. Sačuvajte ih na sigurnom mestu.",
"disable_two_factor_authentication" => "Onemogući dvofaktorsku autentifikaciju",
"totp_code" => "Kod za dvofaktorsku autentifikaciju",
"monthly_budget" => "Mesečni budžet",
"budget_info" => "Ovo je vaš mesečni budžet za sve pretplate. Ovo je samo informativno i ne ograničava vas.",
"household" => "Domaćinstvo",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Oktobar",
"month-11" => "Novembar",
"month-12" => "Decembar",
// TOTP Page
"insert_totp_code" => "Unesite TOTP kod",
];

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "Avatarı yükle",
'file_type_error' => "Dosya türü izin verilmiyor",
'user_details' => "Kullanıcı Detayları",
'two_factor_authentication' => "İki Faktörlü Kimlik Doğrulama",
'two_factor_info' => "İki Faktörlü Kimlik Doğrulama, hesabınıza ekstra bir güvenlik katmanı ekler.<br>Karekodu taramak için Google Authenticator, Authy veya Ente Auth gibi bir kimlik doğrulayıcı uygulamasına ihtiyacınız olacaktır.",
'two_factor_enabled_info' => "Hesabınız İki Faktörlü Kimlik Doğrulama ile güvendedir. Yukarıdaki düğmeye tıklayarak devre dışı bırakabilirsiniz.",
"enable_two_factor_authentication" => "İki Faktörlü Kimlik Doğrulamayı Etkinleştir",
"2fa_already_enabled" => "İki Faktörlü Kimlik Doğrulama zaten etkinleştirildi",
"totp_code_incorrect" => "TOTP kodu yanlış",
"backup_codes" => "Yedek Kodlar",
"download_backup_codes" => "Yedek Kodları İndir",
"copy_to_clipboard" => "Panoya Kopyala",
"totp_backup_codes_info" => "Yedek kodları güvenli bir yerde saklayın. Her biri yalnızca bir kez kullanılabilir.",
"disable_two_factor_authentication" => "İki Faktörlü Kimlik Doğrulamayı Devre Dışı Bırak",
"totp_code" => "TOTP Kodu",
"monthly_budget" => "Aylık Bütçe",
"budget_info" => "Bir bütçe belirlemek, istatistik sayfasında bütçe ve gerçek harcamaları karşılaştırmanıza olanak tanır.",
"household" => "Hane",
@@ -344,6 +356,8 @@ $i18n = [
"month-10" => "Ekim",
"month-11" => "Kasım",
"month-12" => "Aralık",
// TOTP Page
"insert_totp_code" => "Lütfen TOTP kodunuzu girin",
];

View File

@@ -128,6 +128,18 @@ $i18n = [
'upload_avatar' => "上传头像",
'file_type_error' => "文件类型不允许",
'user_details' => "用户详情",
'two_factor_authentication' => "双因素认证",
'two_factor_info' => "双因素身份验证为您的账户增加了一层额外的安全保护。您需要使用 Google Authenticator、Authy 或 Ente Auth 等认证程序来扫描二维码。",
'two_factor_enabled_info' => "双因素身份验证确保您的账户安全。您可以单击上面的按钮禁用它。",
"enable_two_factor_authentication" => "启用双因素身份验证",
"2fa_already_enabled" => "双因素身份验证已启用",
"totp_code_incorrect" => "TOTP 代码不正确",
"backup_codes" => "备份代码",
"download_backup_codes" => "下载备份代码",
"copy_to_clipboard" => "复制到剪贴板",
"totp_backup_codes_info" => "请务必保存这些备份代码。如果您丢失了双因素身份验证设备,您将需要这些备份代码来登录。",
"disable_two_factor_authentication" => "禁用双因素身份验证",
"totp_code" => "TOTP 代码",
"monthly_budget" => "每月预算",
"budget_info" => "设置预算后,您可以在统计页面上比较预算和实际支出。",
"household" => "家庭",
@@ -365,6 +377,9 @@ $i18n = [
"month-11" => "十一月",
"month-12" => "十二月",
// TOTP Page
"insert_totp_code" => "请输入 TOTP 代码",
];
?>

View File

@@ -120,6 +120,18 @@ $i18n = [
'upload_avatar' => "上传头像",
'file_type_error' => "文件类型不允许",
'user_details' => "使用者詳細資訊",
'two_factor_authentication' => "雙因素驗證",
'two_factor_info' => "雙因素驗證為您的帳戶增加了一層額外的安全性。您需要使用 Google Authenticator、Authy 或 Ente Auth 等驗證器應用程式來掃描 QR 代碼。",
'two_factor_enabled_info' => "您的帳戶有雙重認證,十分安全。您可以按一下上面的按鈕停用它。",
"enable_two_factor_authentication" => "啟用雙因素驗證",
"2fa_already_enabled" => "雙因素驗證已經啟用",
"totp_code_incorrect" => "TOTP 驗證碼不正確",
"backup_codes" => "備份代碼",
"download_backup_codes" => "下載備份代碼",
"copy_to_clipboard" => "複製到剪貼板",
"totp_backup_codes_info" => "請妥善保管這些代碼。當您無法使用雙因素驗證應用程式時,您可以使用這些代碼來登入。",
"disable_two_factor_authentication" => "停用雙因素驗證",
"totp_code" => "TOTP 驗證碼",
"monthly_budget" => "每月預算",
"budget_info" => "設定預算後,您可以在統計頁面上比較預算和實際支出。",
"household" => "家庭",
@@ -345,6 +357,9 @@ $i18n = [
"month-11" => "十一月",
"month-12" => "十二月",
// TOTP Page
"insert_totp_code" => "請輸入 TOTP 驗證碼",
];

View File

@@ -1,3 +1,3 @@
<?php
$version = "v2.24.1";
$version = "v2.25.0";
?>

104
libs/OTPHP/Factory.php Normal file
View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use Psr\Clock\ClockInterface;
use Throwable;
use function assert;
use function count;
/**
* This class is used to load OTP object from a provisioning Uri.
*
* @see \OTPHP\Test\FactoryTest
*/
final class Factory implements FactoryInterface
{
public static function loadFromProvisioningUri(string $uri, ?ClockInterface $clock = null): OTPInterface
{
try {
$parsed_url = Url::fromString($uri);
$parsed_url->getScheme() === 'otpauth' || throw new InvalidArgumentException('Invalid scheme.');
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Not a valid OTP provisioning URI', $throwable->getCode(), $throwable);
}
if ($clock === null) {
trigger_deprecation(
'spomky-labs/otphp',
'11.3.0',
'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}
$otp = self::createOTP($parsed_url, $clock);
self::populateOTP($otp, $parsed_url);
return $otp;
}
private static function populateParameters(OTPInterface $otp, Url $data): void
{
foreach ($data->getQuery() as $key => $value) {
$otp->setParameter($key, $value);
}
}
private static function populateOTP(OTPInterface $otp, Url $data): void
{
self::populateParameters($otp, $data);
$result = explode(':', rawurldecode(mb_substr($data->getPath(), 1)));
if (count($result) < 2) {
$otp->setIssuerIncludedAsParameter(false);
return;
}
if ($otp->getIssuer() !== null) {
$result[0] === $otp->getIssuer() || throw new InvalidArgumentException(
'Invalid OTP: invalid issuer in parameter'
);
$otp->setIssuerIncludedAsParameter(true);
}
assert($result[0] !== '');
$otp->setIssuer($result[0]);
}
private static function createOTP(Url $parsed_url, ClockInterface $clock): OTPInterface
{
switch ($parsed_url->getHost()) {
case 'totp':
$totp = TOTP::createFromSecret($parsed_url->getSecret(), $clock);
$totp->setLabel(self::getLabel($parsed_url->getPath()));
return $totp;
case 'hotp':
$hotp = HOTP::createFromSecret($parsed_url->getSecret());
$hotp->setLabel(self::getLabel($parsed_url->getPath()));
return $hotp;
default:
throw new InvalidArgumentException(sprintf('Unsupported "%s" OTP type', $parsed_url->getHost()));
}
}
/**
* @param non-empty-string $data
* @return non-empty-string
*/
private static function getLabel(string $data): string
{
$result = explode(':', rawurldecode(mb_substr($data, 1)));
$label = count($result) === 2 ? $result[1] : $result[0];
assert($label !== '');
return $label;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace OTPHP;
interface FactoryInterface
{
/**
* This method is the unique public method of the class. It can load a provisioning Uri and convert it into an OTP
* object.
*
* @param non-empty-string $uri
*/
public static function loadFromProvisioningUri(string $uri): OTPInterface;
}

137
libs/OTPHP/HOTP.php Normal file
View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use function is_int;
/**
* @see \OTPHP\Test\HOTPTest
*/
final class HOTP extends OTP implements HOTPInterface
{
private const DEFAULT_WINDOW = 0;
public static function create(
null|string $secret = null,
int $counter = self::DEFAULT_COUNTER,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS
): self {
$htop = $secret !== null
? self::createFromSecret($secret)
: self::generate()
;
$htop->setCounter($counter);
$htop->setDigest($digest);
$htop->setDigits($digits);
return $htop;
}
public static function createFromSecret(string $secret): self
{
$htop = new self($secret);
$htop->setCounter(self::DEFAULT_COUNTER);
$htop->setDigest(self::DEFAULT_DIGEST);
$htop->setDigits(self::DEFAULT_DIGITS);
return $htop;
}
public static function generate(): self
{
return self::createFromSecret(self::generateSecret());
}
/**
* @return 0|positive-int
*/
public function getCounter(): int
{
$value = $this->getParameter('counter');
(is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "counter" parameter.');
return $value;
}
public function getProvisioningUri(): string
{
return $this->generateURI('hotp', [
'counter' => $this->getCounter(),
]);
}
/**
* If the counter is not provided, the OTP is verified at the actual counter.
*
* @param null|0|positive-int $counter
*/
public function verify(string $otp, null|int $counter = null, null|int $window = null): bool
{
$counter >= 0 || throw new InvalidArgumentException('The counter must be at least 0.');
if ($counter === null) {
$counter = $this->getCounter();
} elseif ($counter < $this->getCounter()) {
return false;
}
return $this->verifyOtpWithWindow($otp, $counter, $window);
}
public function setCounter(int $counter): void
{
$this->setParameter('counter', $counter);
}
/**
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [...parent::getParameterMap(), ...[
'counter' => static function (mixed $value): int {
$value = (int) $value;
$value >= 0 || throw new InvalidArgumentException('Counter must be at least 0.');
return $value;
},
]];
}
private function updateCounter(int $counter): void
{
$this->setCounter($counter);
}
/**
* @param null|0|positive-int $window
*/
private function getWindow(null|int $window): int
{
return abs($window ?? self::DEFAULT_WINDOW);
}
/**
* @param non-empty-string $otp
* @param 0|positive-int $counter
* @param null|0|positive-int $window
*/
private function verifyOtpWithWindow(string $otp, int $counter, null|int $window): bool
{
$window = $this->getWindow($window);
for ($i = $counter; $i <= $counter + $window; ++$i) {
if ($this->compareOTP($this->at($i), $otp)) {
$this->updateCounter($i + 1);
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace OTPHP;
interface HOTPInterface extends OTPInterface
{
public const DEFAULT_COUNTER = 0;
/**
* The initial counter (a positive integer).
*/
public function getCounter(): int;
/**
* Create a new HOTP object.
*
* If the secret is null, a random 64 bytes secret will be generated.
*
* @param null|non-empty-string $secret
* @param 0|positive-int $counter
* @param non-empty-string $digest
* @param positive-int $digits
*
* @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
*/
public static function create(
null|string $secret = null,
int $counter = 0,
string $digest = 'sha1',
int $digits = 6
): self;
public function setCounter(int $counter): void;
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use DateTimeImmutable;
use Psr\Clock\ClockInterface;
/**
* @internal
*/
final class InternalClock implements ClockInterface
{
public function now(): DateTimeImmutable
{
return new DateTimeImmutable();
}
}

150
libs/OTPHP/OTP.php Normal file
View File

@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use Exception;
use InvalidArgumentException;
use ParagonIE\ConstantTime\Base32;
use RuntimeException;
use function assert;
use function chr;
use function count;
use function is_string;
use const STR_PAD_LEFT;
abstract class OTP implements OTPInterface
{
use ParameterTrait;
private const DEFAULT_SECRET_SIZE = 64;
/**
* @param non-empty-string $secret
*/
protected function __construct(string $secret)
{
$this->setSecret($secret);
}
public function getQrCodeUri(string $uri, string $placeholder): string
{
$provisioning_uri = urlencode($this->getProvisioningUri());
return str_replace($placeholder, $provisioning_uri, $uri);
}
/**
* @param 0|positive-int $input
*/
public function at(int $input): string
{
return $this->generateOTP($input);
}
/**
* @return non-empty-string
*/
final protected static function generateSecret(): string
{
return Base32::encodeUpper(random_bytes(self::DEFAULT_SECRET_SIZE));
}
/**
* The OTP at the specified input.
*
* @param 0|positive-int $input
*
* @return non-empty-string
*/
protected function generateOTP(int $input): string
{
$hash = hash_hmac($this->getDigest(), $this->intToByteString($input), $this->getDecodedSecret(), true);
$unpacked = unpack('C*', $hash);
$unpacked !== false || throw new InvalidArgumentException('Invalid data.');
$hmac = array_values($unpacked);
$offset = ($hmac[count($hmac) - 1] & 0xF);
$code = ($hmac[$offset] & 0x7F) << 24 | ($hmac[$offset + 1] & 0xFF) << 16 | ($hmac[$offset + 2] & 0xFF) << 8 | ($hmac[$offset + 3] & 0xFF);
$otp = $code % (10 ** $this->getDigits());
return str_pad((string) $otp, $this->getDigits(), '0', STR_PAD_LEFT);
}
/**
* @param array<non-empty-string, mixed> $options
*/
protected function filterOptions(array &$options): void
{
foreach ([
'algorithm' => 'sha1',
'period' => 30,
'digits' => 6,
] as $key => $default) {
if (isset($options[$key]) && $default === $options[$key]) {
unset($options[$key]);
}
}
ksort($options);
}
/**
* @param non-empty-string $type
* @param array<non-empty-string, mixed> $options
*
* @return non-empty-string
*/
protected function generateURI(string $type, array $options): string
{
$label = $this->getLabel();
is_string($label) || throw new InvalidArgumentException('The label is not set.');
$this->hasColon($label) === false || throw new InvalidArgumentException('Label must not contain a colon.');
$options = [...$options, ...$this->getParameters()];
$this->filterOptions($options);
$params = str_replace(['+', '%7E'], ['%20', '~'], http_build_query($options, '', '&'));
return sprintf(
'otpauth://%s/%s?%s',
$type,
rawurlencode(($this->getIssuer() !== null ? $this->getIssuer() . ':' : '') . $label),
$params
);
}
/**
* @param non-empty-string $safe
* @param non-empty-string $user
*/
protected function compareOTP(string $safe, string $user): bool
{
return hash_equals($safe, $user);
}
/**
* @return non-empty-string
*/
private function getDecodedSecret(): string
{
try {
$decoded = Base32::decodeUpper($this->getSecret());
} catch (Exception) {
throw new RuntimeException('Unable to decode the secret. Is it correctly base32 encoded?');
}
assert($decoded !== '');
return $decoded;
}
private function intToByteString(int $int): string
{
$result = [];
while ($int !== 0) {
$result[] = chr($int & 0xFF);
$int >>= 8;
}
return str_pad(implode('', array_reverse($result)), 8, "\000", STR_PAD_LEFT);
}
}

132
libs/OTPHP/OTPInterface.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace OTPHP;
interface OTPInterface
{
public const DEFAULT_DIGITS = 6;
public const DEFAULT_DIGEST = 'sha1';
/**
* Create a OTP object from an existing secret.
*
* @param non-empty-string $secret
*/
public static function createFromSecret(string $secret): self;
/**
* Create a new OTP object. A random 64 bytes secret will be generated.
*/
public static function generate(): self;
/**
* @param non-empty-string $secret
*/
public function setSecret(string $secret): void;
public function setDigits(int $digits): void;
/**
* @param non-empty-string $digest
*/
public function setDigest(string $digest): void;
/**
* Generate the OTP at the specified input.
*
* @param 0|positive-int $input
*
* @return non-empty-string Return the OTP at the specified timestamp
*/
public function at(int $input): string;
/**
* Verify that the OTP is valid with the specified input. If no input is provided, the input is set to a default
* value or false is returned.
*
* @param non-empty-string $otp
* @param null|0|positive-int $input
* @param null|0|positive-int $window
*/
public function verify(string $otp, null|int $input = null, null|int $window = null): bool;
/**
* @return non-empty-string The secret of the OTP
*/
public function getSecret(): string;
/**
* @param non-empty-string $label The label of the OTP
*/
public function setLabel(string $label): void;
/**
* @return non-empty-string|null The label of the OTP
*/
public function getLabel(): null|string;
/**
* @return non-empty-string|null The issuer
*/
public function getIssuer(): ?string;
/**
* @param non-empty-string $issuer
*/
public function setIssuer(string $issuer): void;
/**
* @return bool If true, the issuer will be added as a parameter in the provisioning URI
*/
public function isIssuerIncludedAsParameter(): bool;
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void;
/**
* @return positive-int Number of digits in the OTP
*/
public function getDigits(): int;
/**
* @return non-empty-string Digest algorithm used to calculate the OTP. Possible values are 'md5', 'sha1', 'sha256' and 'sha512'
*/
public function getDigest(): string;
/**
* @param non-empty-string $parameter
*/
public function getParameter(string $parameter): mixed;
/**
* @param non-empty-string $parameter
*/
public function hasParameter(string $parameter): bool;
/**
* @return array<non-empty-string, mixed>
*/
public function getParameters(): array;
/**
* @param non-empty-string $parameter
*/
public function setParameter(string $parameter, mixed $value): void;
/**
* Get the provisioning URI.
*
* @return non-empty-string
*/
public function getProvisioningUri(): string;
/**
* Get the provisioning URI.
*
* @param non-empty-string $uri The Uri of the QRCode generator with all parameters. This Uri MUST contain a placeholder that will be replaced by the method.
* @param non-empty-string $placeholder the placeholder to be replaced in the QR Code generator URI
*/
public function getQrCodeUri(string $uri, string $placeholder): string;
}

View File

@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use function array_key_exists;
use function assert;
use function in_array;
use function is_int;
use function is_string;
trait ParameterTrait
{
/**
* @var array<non-empty-string, mixed>
*/
private array $parameters = [];
/**
* @var non-empty-string|null
*/
private null|string $issuer = null;
/**
* @var non-empty-string|null
*/
private null|string $label = null;
private bool $issuer_included_as_parameter = true;
/**
* @return array<non-empty-string, mixed>
*/
public function getParameters(): array
{
$parameters = $this->parameters;
if ($this->getIssuer() !== null && $this->isIssuerIncludedAsParameter() === true) {
$parameters['issuer'] = $this->getIssuer();
}
return $parameters;
}
public function getSecret(): string
{
$value = $this->getParameter('secret');
(is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "secret" parameter.');
return $value;
}
public function getLabel(): null|string
{
return $this->label;
}
public function setLabel(string $label): void
{
$this->setParameter('label', $label);
}
public function getIssuer(): null|string
{
return $this->issuer;
}
public function setIssuer(string $issuer): void
{
$this->setParameter('issuer', $issuer);
}
public function isIssuerIncludedAsParameter(): bool
{
return $this->issuer_included_as_parameter;
}
public function setIssuerIncludedAsParameter(bool $issuer_included_as_parameter): void
{
$this->issuer_included_as_parameter = $issuer_included_as_parameter;
}
public function getDigits(): int
{
$value = $this->getParameter('digits');
(is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "digits" parameter.');
return $value;
}
public function getDigest(): string
{
$value = $this->getParameter('algorithm');
(is_string($value) && $value !== '') || throw new InvalidArgumentException('Invalid "algorithm" parameter.');
return $value;
}
public function hasParameter(string $parameter): bool
{
return array_key_exists($parameter, $this->parameters);
}
public function getParameter(string $parameter): mixed
{
if ($this->hasParameter($parameter)) {
return $this->getParameters()[$parameter];
}
throw new InvalidArgumentException(sprintf('Parameter "%s" does not exist', $parameter));
}
public function setParameter(string $parameter, mixed $value): void
{
$map = $this->getParameterMap();
if (array_key_exists($parameter, $map) === true) {
$callback = $map[$parameter];
$value = $callback($value);
}
if (property_exists($this, $parameter)) {
$this->{$parameter} = $value;
} else {
$this->parameters[$parameter] = $value;
}
}
public function setSecret(string $secret): void
{
$this->setParameter('secret', $secret);
}
public function setDigits(int $digits): void
{
$this->setParameter('digits', $digits);
}
public function setDigest(string $digest): void
{
$this->setParameter('algorithm', $digest);
}
/**
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [
'label' => function (string $value): string {
assert($value !== '');
$this->hasColon($value) === false || throw new InvalidArgumentException(
'Label must not contain a colon.'
);
return $value;
},
'secret' => static fn (string $value): string => mb_strtoupper(trim($value, '=')),
'algorithm' => static function (string $value): string {
$value = mb_strtolower($value);
in_array($value, hash_algos(), true) || throw new InvalidArgumentException(sprintf(
'The "%s" digest is not supported.',
$value
));
return $value;
},
'digits' => static function ($value): int {
$value > 0 || throw new InvalidArgumentException('Digits must be at least 1.');
return (int) $value;
},
'issuer' => function (string $value): string {
assert($value !== '');
$this->hasColon($value) === false || throw new InvalidArgumentException(
'Issuer must not contain a colon.'
);
return $value;
},
];
}
/**
* @param non-empty-string $value
*/
private function hasColon(string $value): bool
{
$colons = [':', '%3A', '%3a'];
foreach ($colons as $colon) {
if (str_contains($value, $colon)) {
return true;
}
}
return false;
}
}

215
libs/OTPHP/TOTP.php Normal file
View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use Psr\Clock\ClockInterface;
use function assert;
use function is_int;
/**
* @see \OTPHP\Test\TOTPTest
*/
final class TOTP extends OTP implements TOTPInterface
{
private readonly ClockInterface $clock;
public function __construct(string $secret, ?ClockInterface $clock = null)
{
parent::__construct($secret);
if ($clock === null) {
trigger_deprecation(
'spomky-labs/otphp',
'11.3.0',
'The parameter "$clock" will become mandatory in 12.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}
$this->clock = $clock;
}
public static function create(
null|string $secret = null,
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS,
int $epoch = self::DEFAULT_EPOCH,
?ClockInterface $clock = null
): self {
$totp = $secret !== null
? self::createFromSecret($secret, $clock)
: self::generate($clock)
;
$totp->setPeriod($period);
$totp->setDigest($digest);
$totp->setDigits($digits);
$totp->setEpoch($epoch);
return $totp;
}
public static function createFromSecret(string $secret, ?ClockInterface $clock = null): self
{
$totp = new self($secret, $clock);
$totp->setPeriod(self::DEFAULT_PERIOD);
$totp->setDigest(self::DEFAULT_DIGEST);
$totp->setDigits(self::DEFAULT_DIGITS);
$totp->setEpoch(self::DEFAULT_EPOCH);
return $totp;
}
public static function generate(?ClockInterface $clock = null): self
{
return self::createFromSecret(self::generateSecret(), $clock);
}
public function getPeriod(): int
{
$value = $this->getParameter('period');
(is_int($value) && $value > 0) || throw new InvalidArgumentException('Invalid "period" parameter.');
return $value;
}
public function getEpoch(): int
{
$value = $this->getParameter('epoch');
(is_int($value) && $value >= 0) || throw new InvalidArgumentException('Invalid "epoch" parameter.');
return $value;
}
public function expiresIn(): int
{
$period = $this->getPeriod();
return $period - ($this->clock->now()->getTimestamp() % $this->getPeriod());
}
/**
* The OTP at the specified input.
*
* @param 0|positive-int $input
*/
public function at(int $input): string
{
return $this->generateOTP($this->timecode($input));
}
public function now(): string
{
$timestamp = $this->clock->now()
->getTimestamp();
assert($timestamp >= 0, 'The timestamp must return a positive integer.');
return $this->at($timestamp);
}
/**
* If no timestamp is provided, the OTP is verified at the actual timestamp. When used, the leeway parameter will
* allow time drift. The passed value is in seconds.
*
* @param 0|positive-int $timestamp
* @param null|0|positive-int $leeway
*/
public function verify(string $otp, null|int $timestamp = null, null|int $leeway = null): bool
{
$timestamp ??= $this->clock->now()
->getTimestamp();
$timestamp >= 0 || throw new InvalidArgumentException('Timestamp must be at least 0.');
if ($leeway === null) {
return $this->compareOTP($this->at($timestamp), $otp);
}
$leeway = abs($leeway);
$leeway < $this->getPeriod() || throw new InvalidArgumentException(
'The leeway must be lower than the TOTP period'
);
$timestampMinusLeeway = $timestamp - $leeway;
$timestampMinusLeeway >= 0 || throw new InvalidArgumentException(
'The timestamp must be greater than or equal to the leeway.'
);
return $this->compareOTP($this->at($timestampMinusLeeway), $otp)
|| $this->compareOTP($this->at($timestamp), $otp)
|| $this->compareOTP($this->at($timestamp + $leeway), $otp);
}
public function getProvisioningUri(): string
{
$params = [];
if ($this->getPeriod() !== 30) {
$params['period'] = $this->getPeriod();
}
if ($this->getEpoch() !== 0) {
$params['epoch'] = $this->getEpoch();
}
return $this->generateURI('totp', $params);
}
public function setPeriod(int $period): void
{
$this->setParameter('period', $period);
}
public function setEpoch(int $epoch): void
{
$this->setParameter('epoch', $epoch);
}
/**
* @return array<non-empty-string, callable>
*/
protected function getParameterMap(): array
{
return [
...parent::getParameterMap(),
'period' => static function ($value): int {
(int) $value > 0 || throw new InvalidArgumentException('Period must be at least 1.');
return (int) $value;
},
'epoch' => static function ($value): int {
(int) $value >= 0 || throw new InvalidArgumentException(
'Epoch must be greater than or equal to 0.'
);
return (int) $value;
},
];
}
/**
* @param array<non-empty-string, mixed> $options
*/
protected function filterOptions(array &$options): void
{
parent::filterOptions($options);
if (isset($options['epoch']) && $options['epoch'] === 0) {
unset($options['epoch']);
}
ksort($options);
}
/**
* @param 0|positive-int $timestamp
*
* @return 0|positive-int
*/
private function timecode(int $timestamp): int
{
$timecode = (int) floor(($timestamp - $this->getEpoch()) / $this->getPeriod());
assert($timecode >= 0);
return $timecode;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace OTPHP;
interface TOTPInterface extends OTPInterface
{
public const DEFAULT_PERIOD = 30;
public const DEFAULT_EPOCH = 0;
/**
* Create a new TOTP object.
*
* If the secret is null, a random 64 bytes secret will be generated.
*
* @param null|non-empty-string $secret
* @param positive-int $period
* @param non-empty-string $digest
* @param positive-int $digits
*
* @deprecated Deprecated since v11.1, use ::createFromSecret or ::generate instead
*/
public static function create(
null|string $secret = null,
int $period = self::DEFAULT_PERIOD,
string $digest = self::DEFAULT_DIGEST,
int $digits = self::DEFAULT_DIGITS
): self;
public function setPeriod(int $period): void;
public function setEpoch(int $epoch): void;
/**
* Return the TOTP at the current time.
*
* @return non-empty-string
*/
public function now(): string;
/**
* Get the period of time for OTP generation (a non-null positive integer, in second).
*/
public function getPeriod(): int;
public function expiresIn(): int;
public function getEpoch(): int;
}

102
libs/OTPHP/Url.php Normal file
View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace OTPHP;
use InvalidArgumentException;
use function array_key_exists;
use function is_string;
/**
* @internal
*/
final class Url
{
/**
* @param non-empty-string $scheme
* @param non-empty-string $host
* @param non-empty-string $path
* @param non-empty-string $secret
* @param array<non-empty-string, mixed> $query
*/
public function __construct(
private readonly string $scheme,
private readonly string $host,
private readonly string $path,
private readonly string $secret,
private readonly array $query
) {
}
/**
* @return non-empty-string
*/
public function getScheme(): string
{
return $this->scheme;
}
/**
* @return non-empty-string
*/
public function getHost(): string
{
return $this->host;
}
/**
* @return non-empty-string
*/
public function getPath(): string
{
return $this->path;
}
/**
* @return non-empty-string
*/
public function getSecret(): string
{
return $this->secret;
}
/**
* @return array<non-empty-string, mixed>
*/
public function getQuery(): array
{
return $this->query;
}
/**
* @param non-empty-string $uri
*/
public static function fromString(string $uri): self
{
$parsed_url = parse_url($uri);
$parsed_url !== false || throw new InvalidArgumentException('Invalid URI.');
foreach (['scheme', 'host', 'path', 'query'] as $key) {
array_key_exists($key, $parsed_url) || throw new InvalidArgumentException(
'Not a valid OTP provisioning URI'
);
}
$scheme = $parsed_url['scheme'] ?? null;
$host = $parsed_url['host'] ?? null;
$path = $parsed_url['path'] ?? null;
$query = $parsed_url['query'] ?? null;
$scheme === 'otpauth' || throw new InvalidArgumentException('Not a valid OTP provisioning URI');
is_string($host) || throw new InvalidArgumentException('Invalid URI.');
is_string($path) || throw new InvalidArgumentException('Invalid URI.');
is_string($query) || throw new InvalidArgumentException('Invalid URI.');
$parsedQuery = [];
parse_str($query, $parsedQuery);
array_key_exists('secret', $parsedQuery) || throw new InvalidArgumentException(
'Not a valid OTP provisioning URI'
);
$secret = $parsedQuery['secret'];
unset($parsedQuery['secret']);
return new self($scheme, $host, $path, $secret, $parsedQuery);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Psr\Clock;
use DateTimeImmutable;
interface ClockInterface
{
/**
* Returns the current time as a DateTimeImmutable Object
*/
public function now(): DateTimeImmutable;
}

View File

@@ -0,0 +1,541 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use InvalidArgumentException;
use RangeException;
use TypeError;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base32
* [A-Z][2-7]
*
* @package ParagonIE\ConstantTime
*/
abstract class Base32 implements EncoderInterface
{
/**
* Decode a Base32-encoded string into raw binary
*
* @param string $encodedString
* @param bool $strictPadding
* @return string
*/
public static function decode(
#[\SensitiveParameter]
string $encodedString,
bool $strictPadding = false
): string {
return static::doDecode($encodedString, false, $strictPadding);
}
/**
* Decode an uppercase Base32-encoded string into raw binary
*
* @param string $src
* @param bool $strictPadding
* @return string
*/
public static function decodeUpper(
#[\SensitiveParameter]
string $src,
bool $strictPadding = false
): string {
return static::doDecode($src, true, $strictPadding);
}
/**
* Encode into Base32 (RFC 4648)
*
* @param string $binString
* @return string
* @throws TypeError
*/
public static function encode(
#[\SensitiveParameter]
string $binString
): string {
return static::doEncode($binString, false, true);
}
/**
* Encode into Base32 (RFC 4648)
*
* @param string $src
* @return string
* @throws TypeError
*/
public static function encodeUnpadded(
#[\SensitiveParameter]
string $src
): string {
return static::doEncode($src, false, false);
}
/**
* Encode into uppercase Base32 (RFC 4648)
*
* @param string $src
* @return string
* @throws TypeError
*/
public static function encodeUpper(
#[\SensitiveParameter]
string $src
): string {
return static::doEncode($src, true, true);
}
/**
* Encode into uppercase Base32 (RFC 4648)
*
* @param string $src
* @return string
* @throws TypeError
*/
public static function encodeUpperUnpadded(
#[\SensitiveParameter]
string $src
): string {
return static::doEncode($src, true, false);
}
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* @param int $src
* @return int
*/
protected static function decode5Bits(int $src): int
{
$ret = -1;
// if ($src > 96 && $src < 123) $ret += $src - 97 + 1; // -64
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96);
// if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
$ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* Uppercase variant.
*
* @param int $src
* @return int
*/
protected static function decode5BitsUpper(int $src): int
{
$ret = -1;
// if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
// if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23
$ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* @param int $src
* @return string
*/
protected static function encode5Bits(int $src): string
{
$diff = 0x61;
// if ($src > 25) $ret -= 72;
$diff -= ((25 - $src) >> 8) & 73;
return \pack('C', $src + $diff);
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* Uppercase variant.
*
* @param int $src
* @return string
*/
protected static function encode5BitsUpper(int $src): string
{
$diff = 0x41;
// if ($src > 25) $ret -= 40;
$diff -= ((25 - $src) >> 8) & 41;
return \pack('C', $src + $diff);
}
/**
* @param string $encodedString
* @param bool $upper
* @return string
*/
public static function decodeNoPadding(
#[\SensitiveParameter]
string $encodedString,
bool $upper = false
): string {
$srcLen = Binary::safeStrlen($encodedString);
if ($srcLen === 0) {
return '';
}
if (($srcLen & 7) === 0) {
for ($j = 0; $j < 7 && $j < $srcLen; ++$j) {
if ($encodedString[$srcLen - $j - 1] === '=') {
throw new InvalidArgumentException(
"decodeNoPadding() doesn't tolerate padding"
);
}
}
}
return static::doDecode(
$encodedString,
$upper,
true
);
}
/**
* Base32 decoding
*
* @param string $src
* @param bool $upper
* @param bool $strictPadding
* @return string
*
* @throws TypeError
*/
protected static function doDecode(
#[\SensitiveParameter]
string $src,
bool $upper = false,
bool $strictPadding = false
): string {
// We do this to reduce code duplication:
$method = $upper
? 'decode5BitsUpper'
: 'decode5Bits';
// Remove padding
$srcLen = Binary::safeStrlen($src);
if ($srcLen === 0) {
return '';
}
if ($strictPadding) {
if (($srcLen & 7) === 0) {
for ($j = 0; $j < 7; ++$j) {
if ($src[$srcLen - 1] === '=') {
$srcLen--;
} else {
break;
}
}
}
if (($srcLen & 7) === 1) {
throw new RangeException(
'Incorrect padding'
);
}
} else {
$src = \rtrim($src, '=');
$srcLen = Binary::safeStrlen($src);
}
$err = 0;
$dest = '';
// Main loop (no padding):
for ($i = 0; $i + 8 <= $srcLen; $i += 8) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, 8));
/** @var int $c0 */
$c0 = static::$method($chunk[1]);
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
/** @var int $c4 */
$c4 = static::$method($chunk[5]);
/** @var int $c5 */
$c5 = static::$method($chunk[6]);
/** @var int $c6 */
$c6 = static::$method($chunk[7]);
/** @var int $c7 */
$c7 = static::$method($chunk[8]);
$dest .= \pack(
'CCCCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff,
(($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff,
(($c6 << 5) | ($c7 ) ) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8;
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
/** @var int $c0 */
$c0 = static::$method($chunk[1]);
if ($i + 6 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
/** @var int $c4 */
$c4 = static::$method($chunk[5]);
/** @var int $c5 */
$c5 = static::$method($chunk[6]);
/** @var int $c6 */
$c6 = static::$method($chunk[7]);
$dest .= \pack(
'CCCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff,
(($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8;
if ($strictPadding) {
$err |= ($c6 << 5) & 0xff;
}
} elseif ($i + 5 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
/** @var int $c4 */
$c4 = static::$method($chunk[5]);
/** @var int $c5 */
$c5 = static::$method($chunk[6]);
$dest .= \pack(
'CCCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff,
(($c4 << 7) | ($c5 << 2) ) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8;
} elseif ($i + 4 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
/** @var int $c4 */
$c4 = static::$method($chunk[5]);
$dest .= \pack(
'CCC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff,
(($c3 << 4) | ($c4 >> 1) ) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8;
if ($strictPadding) {
$err |= ($c4 << 7) & 0xff;
}
} elseif ($i + 3 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
/** @var int $c3 */
$c3 = static::$method($chunk[4]);
$dest .= \pack(
'CC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff
);
$err |= ($c0 | $c1 | $c2 | $c3) >> 8;
if ($strictPadding) {
$err |= ($c3 << 4) & 0xff;
}
} elseif ($i + 2 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
/** @var int $c2 */
$c2 = static::$method($chunk[3]);
$dest .= \pack(
'CC',
(($c0 << 3) | ($c1 >> 2) ) & 0xff,
(($c1 << 6) | ($c2 << 1) ) & 0xff
);
$err |= ($c0 | $c1 | $c2) >> 8;
if ($strictPadding) {
$err |= ($c2 << 6) & 0xff;
}
} elseif ($i + 1 < $srcLen) {
/** @var int $c1 */
$c1 = static::$method($chunk[2]);
$dest .= \pack(
'C',
(($c0 << 3) | ($c1 >> 2) ) & 0xff
);
$err |= ($c0 | $c1) >> 8;
if ($strictPadding) {
$err |= ($c1 << 6) & 0xff;
}
} else {
$dest .= \pack(
'C',
(($c0 << 3) ) & 0xff
);
$err |= ($c0) >> 8;
}
}
$check = ($err === 0);
if (!$check) {
throw new RangeException(
'Base32::doDecode() only expects characters in the correct base32 alphabet'
);
}
return $dest;
}
/**
* Base32 Encoding
*
* @param string $src
* @param bool $upper
* @param bool $pad
* @return string
* @throws TypeError
*/
protected static function doEncode(
#[\SensitiveParameter]
string $src,
bool $upper = false,
bool $pad = true
): string {
// We do this to reduce code duplication:
$method = $upper
? 'encode5BitsUpper'
: 'encode5Bits';
$dest = '';
$srcLen = Binary::safeStrlen($src);
// Main loop (no padding):
for ($i = 0; $i + 5 <= $srcLen; $i += 5) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, 5));
$b0 = $chunk[1];
$b1 = $chunk[2];
$b2 = $chunk[3];
$b3 = $chunk[4];
$b4 = $chunk[5];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
static::$method((($b3 >> 2) ) & 31) .
static::$method((($b3 << 3) | ($b4 >> 5)) & 31) .
static::$method( $b4 & 31);
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
$b0 = $chunk[1];
if ($i + 3 < $srcLen) {
$b1 = $chunk[2];
$b2 = $chunk[3];
$b3 = $chunk[4];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
static::$method((($b2 << 1) | ($b3 >> 7)) & 31) .
static::$method((($b3 >> 2) ) & 31) .
static::$method((($b3 << 3) ) & 31);
if ($pad) {
$dest .= '=';
}
} elseif ($i + 2 < $srcLen) {
$b1 = $chunk[2];
$b2 = $chunk[3];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) | ($b2 >> 4)) & 31) .
static::$method((($b2 << 1) ) & 31);
if ($pad) {
$dest .= '===';
}
} elseif ($i + 1 < $srcLen) {
$b1 = $chunk[2];
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method((($b0 << 2) | ($b1 >> 6)) & 31) .
static::$method((($b1 >> 1) ) & 31) .
static::$method((($b1 << 4) ) & 31);
if ($pad) {
$dest .= '====';
}
} else {
$dest .=
static::$method( ($b0 >> 3) & 31) .
static::$method( ($b0 << 2) & 31);
if ($pad) {
$dest .= '======';
}
}
}
return $dest;
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base32Hex
* [0-9][A-V]
*
* @package ParagonIE\ConstantTime
*/
abstract class Base32Hex extends Base32
{
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* @param int $src
* @return int
*/
protected static function decode5Bits(int $src): int
{
$ret = -1;
// if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);
// if ($src > 0x60 && $src < 0x77) ret += $src - 0x61 + 10 + 1; // -86
$ret += (((0x60 - $src) & ($src - 0x77)) >> 8) & ($src - 86);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 5-bit integers
* into 8-bit integers.
*
* @param int $src
* @return int
*/
protected static function decode5BitsUpper(int $src): int
{
$ret = -1;
// if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47);
// if ($src > 0x40 && $src < 0x57) ret += $src - 0x41 + 10 + 1; // -54
$ret += (((0x40 - $src) & ($src - 0x57)) >> 8) & ($src - 54);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* @param int $src
* @return string
*/
protected static function encode5Bits(int $src): string
{
$src += 0x30;
// if ($src > 0x39) $src += 0x61 - 0x3a; // 39
$src += ((0x39 - $src) >> 8) & 39;
return \pack('C', $src);
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 5-bit integers.
*
* Uppercase variant.
*
* @param int $src
* @return string
*/
protected static function encode5BitsUpper(int $src): string
{
$src += 0x30;
// if ($src > 0x39) $src += 0x41 - 0x3a; // 7
$src += ((0x39 - $src) >> 8) & 7;
return \pack('C', $src);
}
}

View File

@@ -0,0 +1,319 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use InvalidArgumentException;
use RangeException;
use TypeError;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base64
* [A-Z][a-z][0-9]+/
*
* @package ParagonIE\ConstantTime
*/
abstract class Base64 implements EncoderInterface
{
/**
* Encode into Base64
*
* Base64 character set "[A-Z][a-z][0-9]+/"
*
* @param string $binString
* @return string
*
* @throws TypeError
*/
public static function encode(
#[\SensitiveParameter]
string $binString
): string {
return static::doEncode($binString, true);
}
/**
* Encode into Base64, no = padding
*
* Base64 character set "[A-Z][a-z][0-9]+/"
*
* @param string $src
* @return string
*
* @throws TypeError
*/
public static function encodeUnpadded(
#[\SensitiveParameter]
string $src
): string {
return static::doEncode($src, false);
}
/**
* @param string $src
* @param bool $pad Include = padding?
* @return string
*
* @throws TypeError
*/
protected static function doEncode(
#[\SensitiveParameter]
string $src,
bool $pad = true
): string {
$dest = '';
$srcLen = Binary::safeStrlen($src);
// Main loop (no padding):
for ($i = 0; $i + 3 <= $srcLen; $i += 3) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, 3));
$b0 = $chunk[1];
$b1 = $chunk[2];
$b2 = $chunk[3];
$dest .=
static::encode6Bits( $b0 >> 2 ) .
static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .
static::encode6Bits( $b2 & 63);
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i));
$b0 = $chunk[1];
if ($i + 1 < $srcLen) {
$b1 = $chunk[2];
$dest .=
static::encode6Bits($b0 >> 2) .
static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
static::encode6Bits(($b1 << 2) & 63);
if ($pad) {
$dest .= '=';
}
} else {
$dest .=
static::encode6Bits( $b0 >> 2) .
static::encode6Bits(($b0 << 4) & 63);
if ($pad) {
$dest .= '==';
}
}
}
return $dest;
}
/**
* decode from base64 into binary
*
* Base64 character set "./[A-Z][a-z][0-9]"
*
* @param string $encodedString
* @param bool $strictPadding
* @return string
*
* @throws RangeException
* @throws TypeError
*/
public static function decode(
#[\SensitiveParameter]
string $encodedString,
bool $strictPadding = false
): string {
// Remove padding
$srcLen = Binary::safeStrlen($encodedString);
if ($srcLen === 0) {
return '';
}
if ($strictPadding) {
if (($srcLen & 3) === 0) {
if ($encodedString[$srcLen - 1] === '=') {
$srcLen--;
if ($encodedString[$srcLen - 1] === '=') {
$srcLen--;
}
}
}
if (($srcLen & 3) === 1) {
throw new RangeException(
'Incorrect padding'
);
}
if ($encodedString[$srcLen - 1] === '=') {
throw new RangeException(
'Incorrect padding'
);
}
} else {
$encodedString = \rtrim($encodedString, '=');
$srcLen = Binary::safeStrlen($encodedString);
}
$err = 0;
$dest = '';
// Main loop (no padding):
for ($i = 0; $i + 4 <= $srcLen; $i += 4) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, 4));
$c0 = static::decode6Bits($chunk[1]);
$c1 = static::decode6Bits($chunk[2]);
$c2 = static::decode6Bits($chunk[3]);
$c3 = static::decode6Bits($chunk[4]);
$dest .= \pack(
'CCC',
((($c0 << 2) | ($c1 >> 4)) & 0xff),
((($c1 << 4) | ($c2 >> 2)) & 0xff),
((($c2 << 6) | $c3 ) & 0xff)
);
$err |= ($c0 | $c1 | $c2 | $c3) >> 8;
}
// The last chunk, which may have padding:
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, $srcLen - $i));
$c0 = static::decode6Bits($chunk[1]);
if ($i + 2 < $srcLen) {
$c1 = static::decode6Bits($chunk[2]);
$c2 = static::decode6Bits($chunk[3]);
$dest .= \pack(
'CC',
((($c0 << 2) | ($c1 >> 4)) & 0xff),
((($c1 << 4) | ($c2 >> 2)) & 0xff)
);
$err |= ($c0 | $c1 | $c2) >> 8;
if ($strictPadding) {
$err |= ($c2 << 6) & 0xff;
}
} elseif ($i + 1 < $srcLen) {
$c1 = static::decode6Bits($chunk[2]);
$dest .= \pack(
'C',
((($c0 << 2) | ($c1 >> 4)) & 0xff)
);
$err |= ($c0 | $c1) >> 8;
if ($strictPadding) {
$err |= ($c1 << 4) & 0xff;
}
} elseif ($strictPadding) {
$err |= 1;
}
}
$check = ($err === 0);
if (!$check) {
throw new RangeException(
'Base64::decode() only expects characters in the correct base64 alphabet'
);
}
return $dest;
}
/**
* @param string $encodedString
* @return string
*/
public static function decodeNoPadding(
#[\SensitiveParameter]
string $encodedString
): string {
$srcLen = Binary::safeStrlen($encodedString);
if ($srcLen === 0) {
return '';
}
if (($srcLen & 3) === 0) {
// If $strLen is not zero, and it is divisible by 4, then it's at least 4.
if ($encodedString[$srcLen - 1] === '=' || $encodedString[$srcLen - 2] === '=') {
throw new InvalidArgumentException(
"decodeNoPadding() doesn't tolerate padding"
);
}
}
return static::decode(
$encodedString,
true
);
}
/**
* Uses bitwise operators instead of table-lookups to turn 6-bit integers
* into 8-bit integers.
*
* Base64 character set:
* [A-Z] [a-z] [0-9] + /
* 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f
*
* @param int $src
* @return int
*/
protected static function decode6Bits(int $src): int
{
$ret = -1;
// if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
// if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
// if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
// if ($src == 0x2b) $ret += 62 + 1;
$ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63;
// if ($src == 0x2f) ret += 63 + 1;
$ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64;
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 6-bit integers.
*
* @param int $src
* @return string
*/
protected static function encode6Bits(int $src): string
{
$diff = 0x41;
// if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
$diff += ((25 - $src) >> 8) & 6;
// if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
$diff -= ((51 - $src) >> 8) & 75;
// if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15
$diff -= ((61 - $src) >> 8) & 15;
// if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3
$diff += ((62 - $src) >> 8) & 3;
return \pack('C', $src + $diff);
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base64DotSlash
* ./[A-Z][a-z][0-9]
*
* @package ParagonIE\ConstantTime
*/
abstract class Base64DotSlash extends Base64
{
/**
* Uses bitwise operators instead of table-lookups to turn 6-bit integers
* into 8-bit integers.
*
* Base64 character set:
* ./ [A-Z] [a-z] [0-9]
* 0x2e-0x2f, 0x41-0x5a, 0x61-0x7a, 0x30-0x39
*
* @param int $src
* @return int
*/
protected static function decode6Bits(int $src): int
{
$ret = -1;
// if ($src > 0x2d && $src < 0x30) ret += $src - 0x2e + 1; // -45
$ret += (((0x2d - $src) & ($src - 0x30)) >> 8) & ($src - 45);
// if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 2 + 1; // -62
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 62);
// if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 28 + 1; // -68
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 68);
// if ($src > 0x2f && $src < 0x3a) ret += $src - 0x30 + 54 + 1; // 7
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 7);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 6-bit integers.
*
* @param int $src
* @return string
*/
protected static function encode6Bits(int $src): string
{
$src += 0x2e;
// if ($src > 0x2f) $src += 0x41 - 0x30; // 17
$src += ((0x2f - $src) >> 8) & 17;
// if ($src > 0x5a) $src += 0x61 - 0x5b; // 6
$src += ((0x5a - $src) >> 8) & 6;
// if ($src > 0x7a) $src += 0x30 - 0x7b; // -75
$src -= ((0x7a - $src) >> 8) & 75;
return \pack('C', $src);
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base64DotSlashOrdered
* ./[0-9][A-Z][a-z]
*
* @package ParagonIE\ConstantTime
*/
abstract class Base64DotSlashOrdered extends Base64
{
/**
* Uses bitwise operators instead of table-lookups to turn 6-bit integers
* into 8-bit integers.
*
* Base64 character set:
* [.-9] [A-Z] [a-z]
* 0x2e-0x39, 0x41-0x5a, 0x61-0x7a
*
* @param int $src
* @return int
*/
protected static function decode6Bits(int $src): int
{
$ret = -1;
// if ($src > 0x2d && $src < 0x3a) ret += $src - 0x2e + 1; // -45
$ret += (((0x2d - $src) & ($src - 0x3a)) >> 8) & ($src - 45);
// if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 12 + 1; // -52
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 52);
// if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 38 + 1; // -58
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 58);
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 6-bit integers.
*
* @param int $src
* @return string
*/
protected static function encode6Bits(int $src): string
{
$src += 0x2e;
// if ($src > 0x39) $src += 0x41 - 0x3a; // 7
$src += ((0x39 - $src) >> 8) & 7;
// if ($src > 0x5a) $src += 0x61 - 0x5b; // 6
$src += ((0x5a - $src) >> 8) & 6;
return \pack('C', $src);
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Base64UrlSafe
* [A-Z][a-z][0-9]\-_
*
* @package ParagonIE\ConstantTime
*/
abstract class Base64UrlSafe extends Base64
{
/**
* Uses bitwise operators instead of table-lookups to turn 6-bit integers
* into 8-bit integers.
*
* Base64 character set:
* [A-Z] [a-z] [0-9] - _
* 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2d, 0x5f
*
* @param int $src
* @return int
*/
protected static function decode6Bits(int $src): int
{
$ret = -1;
// if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
// if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
// if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
// if ($src == 0x2c) $ret += 62 + 1;
$ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63;
// if ($src == 0x5f) ret += 63 + 1;
$ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64;
return $ret;
}
/**
* Uses bitwise operators instead of table-lookups to turn 8-bit integers
* into 6-bit integers.
*
* @param int $src
* @return string
*/
protected static function encode6Bits(int $src): string
{
$diff = 0x41;
// if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6
$diff += ((25 - $src) >> 8) & 6;
// if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75
$diff -= ((51 - $src) >> 8) & 75;
// if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13
$diff -= ((61 - $src) >> 8) & 13;
// if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3
$diff += ((62 - $src) >> 8) & 49;
return \pack('C', $src + $diff);
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use TypeError;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Binary
*
* Binary string operators that don't choke on
* mbstring.func_overload
*
* @package ParagonIE\ConstantTime
*/
abstract class Binary
{
/**
* Safe string length
*
* @ref mbstring.func_overload
*
* @param string $str
* @return int
*/
public static function safeStrlen(
#[\SensitiveParameter]
string $str
): int {
if (\function_exists('mb_strlen')) {
// mb_strlen in PHP 7.x can return false.
/** @psalm-suppress RedundantCast */
return (int) \mb_strlen($str, '8bit');
} else {
return \strlen($str);
}
}
/**
* Safe substring
*
* @ref mbstring.func_overload
*
* @staticvar boolean $exists
* @param string $str
* @param int $start
* @param ?int $length
* @return string
*
* @throws TypeError
*/
public static function safeSubstr(
#[\SensitiveParameter]
string $str,
int $start = 0,
?int $length = null
): string {
if ($length === 0) {
return '';
}
if (\function_exists('mb_substr')) {
return \mb_substr($str, $start, $length, '8bit');
}
// Unlike mb_substr(), substr() doesn't accept NULL for length
if ($length !== null) {
return \substr($str, $start, $length);
} else {
return \substr($str, $start);
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Interface EncoderInterface
* @package ParagonIE\ConstantTime
*/
interface EncoderInterface
{
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $binString (raw binary)
* @return string
*/
public static function encode(string $binString): string;
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $encodedString
* @param bool $strictPadding Error on invalid padding
* @return string (raw binary)
*/
public static function decode(string $encodedString, bool $strictPadding = false): string;
}

View File

@@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use TypeError;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Encoding
* @package ParagonIE\ConstantTime
*/
abstract class Encoding
{
/**
* RFC 4648 Base32 encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32Encode(
#[\SensitiveParameter]
string $str
): string {
return Base32::encode($str);
}
/**
* RFC 4648 Base32 encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32EncodeUpper(
#[\SensitiveParameter]
string $str
): string {
return Base32::encodeUpper($str);
}
/**
* RFC 4648 Base32 decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32Decode(
#[\SensitiveParameter]
string $str
): string {
return Base32::decode($str);
}
/**
* RFC 4648 Base32 decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32DecodeUpper(
#[\SensitiveParameter]
string $str
): string {
return Base32::decodeUpper($str);
}
/**
* RFC 4648 Base32 encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32HexEncode(
#[\SensitiveParameter]
string $str
): string {
return Base32Hex::encode($str);
}
/**
* RFC 4648 Base32Hex encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32HexEncodeUpper(
#[\SensitiveParameter]
string $str
): string {
return Base32Hex::encodeUpper($str);
}
/**
* RFC 4648 Base32Hex decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32HexDecode(
#[\SensitiveParameter]
string $str
): string {
return Base32Hex::decode($str);
}
/**
* RFC 4648 Base32Hex decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base32HexDecodeUpper(
#[\SensitiveParameter]
string $str
): string {
return Base32Hex::decodeUpper($str);
}
/**
* RFC 4648 Base64 encoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base64Encode(
#[\SensitiveParameter]
string $str
): string {
return Base64::encode($str);
}
/**
* RFC 4648 Base64 decoding
*
* @param string $str
* @return string
* @throws TypeError
*/
public static function base64Decode(
#[\SensitiveParameter]
string $str
): string {
return Base64::decode($str);
}
/**
* Encode into Base64
*
* Base64 character set "./[A-Z][a-z][0-9]"
* @param string $str
* @return string
* @throws TypeError
*/
public static function base64EncodeDotSlash(
#[\SensitiveParameter]
string $str
): string {
return Base64DotSlash::encode($str);
}
/**
* Decode from base64 to raw binary
*
* Base64 character set "./[A-Z][a-z][0-9]"
*
* @param string $str
* @return string
* @throws \RangeException
* @throws TypeError
*/
public static function base64DecodeDotSlash(
#[\SensitiveParameter]
string $str
): string {
return Base64DotSlash::decode($str);
}
/**
* Encode into Base64
*
* Base64 character set "[.-9][A-Z][a-z]" or "./[0-9][A-Z][a-z]"
* @param string $str
* @return string
* @throws TypeError
*/
public static function base64EncodeDotSlashOrdered(
#[\SensitiveParameter]
string $str
): string {
return Base64DotSlashOrdered::encode($str);
}
/**
* Decode from base64 to raw binary
*
* Base64 character set "[.-9][A-Z][a-z]" or "./[0-9][A-Z][a-z]"
*
* @param string $str
* @return string
* @throws \RangeException
* @throws TypeError
*/
public static function base64DecodeDotSlashOrdered(
#[\SensitiveParameter]
string $str
): string {
return Base64DotSlashOrdered::decode($str);
}
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $bin_string (raw binary)
* @return string
* @throws TypeError
*/
public static function hexEncode(
#[\SensitiveParameter]
string $bin_string
): string {
return Hex::encode($bin_string);
}
/**
* Convert a hexadecimal string into a binary string without cache-timing
* leaks
*
* @param string $hex_string
* @return string (raw binary)
* @throws \RangeException
*/
public static function hexDecode(
#[\SensitiveParameter]
string $hex_string
): string {
return Hex::decode($hex_string);
}
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $bin_string (raw binary)
* @return string
* @throws TypeError
*/
public static function hexEncodeUpper(
#[\SensitiveParameter]
string $bin_string
): string {
return Hex::encodeUpper($bin_string);
}
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $bin_string (raw binary)
* @return string
*/
public static function hexDecodeUpper(
#[\SensitiveParameter]
string $bin_string
): string {
return Hex::decode($bin_string);
}
}

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use RangeException;
use TypeError;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class Hex
* @package ParagonIE\ConstantTime
*/
abstract class Hex implements EncoderInterface
{
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks
*
* @param string $binString (raw binary)
* @return string
* @throws TypeError
*/
public static function encode(
#[\SensitiveParameter]
string $binString
): string {
$hex = '';
$len = Binary::safeStrlen($binString);
for ($i = 0; $i < $len; ++$i) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C', $binString[$i]);
$c = $chunk[1] & 0xf;
$b = $chunk[1] >> 4;
$hex .= \pack(
'CC',
(87 + $b + ((($b - 10) >> 8) & ~38)),
(87 + $c + ((($c - 10) >> 8) & ~38))
);
}
return $hex;
}
/**
* Convert a binary string into a hexadecimal string without cache-timing
* leaks, returning uppercase letters (as per RFC 4648)
*
* @param string $binString (raw binary)
* @return string
* @throws TypeError
*/
public static function encodeUpper(
#[\SensitiveParameter]
string $binString
): string {
$hex = '';
$len = Binary::safeStrlen($binString);
for ($i = 0; $i < $len; ++$i) {
/** @var array<int, int> $chunk */
$chunk = \unpack('C', $binString[$i]);
$c = $chunk[1] & 0xf;
$b = $chunk[1] >> 4;
$hex .= \pack(
'CC',
(55 + $b + ((($b - 10) >> 8) & ~6)),
(55 + $c + ((($c - 10) >> 8) & ~6))
);
}
return $hex;
}
/**
* Convert a hexadecimal string into a binary string without cache-timing
* leaks
*
* @param string $encodedString
* @param bool $strictPadding
* @return string (raw binary)
* @throws RangeException
*/
public static function decode(
#[\SensitiveParameter]
string $encodedString,
bool $strictPadding = false
): string {
$hex_pos = 0;
$bin = '';
$c_acc = 0;
$hex_len = Binary::safeStrlen($encodedString);
$state = 0;
if (($hex_len & 1) !== 0) {
if ($strictPadding) {
throw new RangeException(
'Expected an even number of hexadecimal characters'
);
} else {
$encodedString = '0' . $encodedString;
++$hex_len;
}
}
/** @var array<int, int> $chunk */
$chunk = \unpack('C*', $encodedString);
while ($hex_pos < $hex_len) {
++$hex_pos;
$c = $chunk[$hex_pos];
$c_num = $c ^ 48;
$c_num0 = ($c_num - 10) >> 8;
$c_alpha = ($c & ~32) - 55;
$c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
if (($c_num0 | $c_alpha0) === 0) {
throw new RangeException(
'Expected hexadecimal character'
);
}
$c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
if ($state === 0) {
$c_acc = $c_val * 16;
} else {
$bin .= \pack('C', $c_acc | $c_val);
}
$state ^= 1;
}
return $bin;
}
}

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace ParagonIE\ConstantTime;
use TypeError;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Class RFC4648
*
* This class conforms strictly to the RFC
*
* @package ParagonIE\ConstantTime
*/
abstract class RFC4648
{
/**
* RFC 4648 Base64 encoding
*
* "foo" -> "Zm9v"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base64Encode(
#[\SensitiveParameter]
string $str
): string {
return Base64::encode($str);
}
/**
* RFC 4648 Base64 decoding
*
* "Zm9v" -> "foo"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base64Decode(
#[\SensitiveParameter]
string $str
): string {
return Base64::decode($str, true);
}
/**
* RFC 4648 Base64 (URL Safe) encoding
*
* "foo" -> "Zm9v"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base64UrlSafeEncode(
#[\SensitiveParameter]
string $str
): string {
return Base64UrlSafe::encode($str);
}
/**
* RFC 4648 Base64 (URL Safe) decoding
*
* "Zm9v" -> "foo"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base64UrlSafeDecode(
#[\SensitiveParameter]
string $str
): string {
return Base64UrlSafe::decode($str, true);
}
/**
* RFC 4648 Base32 encoding
*
* "foo" -> "MZXW6==="
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base32Encode(
#[\SensitiveParameter]
string $str
): string {
return Base32::encodeUpper($str);
}
/**
* RFC 4648 Base32 encoding
*
* "MZXW6===" -> "foo"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base32Decode(
#[\SensitiveParameter]
string $str
): string {
return Base32::decodeUpper($str, true);
}
/**
* RFC 4648 Base32-Hex encoding
*
* "foo" -> "CPNMU==="
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base32HexEncode(
#[\SensitiveParameter]
string $str
): string {
return Base32::encodeUpper($str);
}
/**
* RFC 4648 Base32-Hex decoding
*
* "CPNMU===" -> "foo"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base32HexDecode(
#[\SensitiveParameter]
string $str
): string {
return Base32::decodeUpper($str, true);
}
/**
* RFC 4648 Base16 decoding
*
* "foo" -> "666F6F"
*
* @param string $str
* @return string
*
* @throws TypeError
*/
public static function base16Encode(
#[\SensitiveParameter]
string $str
): string {
return Hex::encodeUpper($str);
}
/**
* RFC 4648 Base16 decoding
*
* "666F6F" -> "foo"
*
* @param string $str
* @return string
*/
public static function base16Decode(
#[\SensitiveParameter]
string $str
): string {
return Hex::decode($str, true);
}
}

View File

@@ -86,6 +86,14 @@ if ($adminRow['login_disabled'] == 1) {
}
}
if (isset($_SESSION['totp_user_id'])) {
unset($_SESSION['totp_user_id']);
}
if (isset($_SESSION['token'])) {
unset($_SESSION['token']);
}
$theme = "light";
$updateThemeSettings = false;
@@ -126,12 +134,42 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
$stmt = $db->prepare($query);
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$verificationRow = $result->fetchArray(SQLITE3_ASSOC);
$verificationMissing = $result->fetchArray(SQLITE3_ASSOC);
if ($verificationRow) {
// Check if the user has 2fa enabled
$query = "SELECT totp_enabled FROM user WHERE id = :userId";
$stmt = $db->prepare($query);
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$totpEnabled = $result->fetchArray(SQLITE3_ASSOC);
if ($verificationMissing) {
$userEmailWaitingVerification = true;
$loginFailed = true;
} else {
if ($rememberMe) {
$token = bin2hex(random_bytes(32));
$addLoginTokens = "INSERT INTO login_tokens (user_id, token) VALUES (:userId, :token)";
$addLoginTokensStmt = $db->prepare($addLoginTokens);
$addLoginTokensStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$addLoginTokensStmt->bindParam(':token', $token, SQLITE3_TEXT);
$addLoginTokensStmt->execute();
$_SESSION['token'] = $token;
$cookieValue = $username . "|" . $token . "|" . $main_currency;
setcookie('wallos_login', $cookieValue, [
'expires' => $cookieExpire,
'samesite' => 'Strict'
]);
}
// Send to totp page if 2fa is enabled
if ($totpEnabled['totp_enabled'] == 1) {
$_SESSION['totp_user_id'] = $userId;
$db->close();
header("Location: totp.php");
exit();
}
$_SESSION['username'] = $username;
$_SESSION['loggedin'] = true;
$_SESSION['main_currency'] = $main_currency;
@@ -148,8 +186,9 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
]);
}
$query = "SELECT color_theme FROM settings";
$query = "SELECT color_theme FROM settings WHERE user_id = :userId";
$stmt = $db->prepare($query);
$stmt->bindValue(':userId', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$settings = $result->fetchArray(SQLITE3_ASSOC);
setcookie('colorTheme', $settings['color_theme'], [
@@ -157,20 +196,6 @@ if (isset($_POST['username']) && isset($_POST['password'])) {
'samesite' => 'Strict'
]);
if ($rememberMe) {
$token = bin2hex(random_bytes(32));
$addLoginTokens = "INSERT INTO login_tokens (user_id, token) VALUES (:userId, :token)";
$addLoginTokensStmt = $db->prepare($addLoginTokens);
$addLoginTokensStmt->bindParam(':userId', $userId, SQLITE3_INTEGER);
$addLoginTokensStmt->bindParam(':token', $token, SQLITE3_TEXT);
$addLoginTokensStmt->execute();
$_SESSION['token'] = $token;
$cookieValue = $username . "|" . $token . "|" . $main_currency;
setcookie('wallos_login', $cookieValue, [
'expires' => $cookieExpire,
'samesite' => 'Strict'
]);
}
$db->close();
header("Location: .");
exit();

21
migrations/000027.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
// this migration adds a "totp_enabled" column to the user table
// it also adds a "totp" table to the database
/** @noinspection PhpUndefinedVariableInspection */
$columnQuery = $db->query("SELECT * FROM pragma_table_info('user') where name='totp_enabled'");
$columnRequired = $columnQuery->fetchArray(SQLITE3_ASSOC) === false;
if ($columnRequired) {
$db->exec('ALTER TABLE user ADD COLUMN totp_enabled BOOLEAN DEFAULT 0');
}
$db->exec('CREATE TABLE IF NOT EXISTS totp (
user_id INTEGER NOT NULL,
totp_secret TEXT NOT NULL,
backup_codes TEXT NOT NULL,
last_totp_used INTEGER DEFAULT 0,
FOREIGN KEY(user_id) REFERENCES user(id)
)');

View File

@@ -37,6 +37,8 @@ let i18n = {
error_sending_notification: "Fehler beim Senden der Benachrichtigung",
delete_account_confirmation: "Möchten Sie Ihr Konto wirklich löschen?",
this_will_delete_all_data: "Dadurch werden alle Daten gelöscht und können nicht wiederhergestellt werden. Fortfahren?",
success: "Erfolg",
copied_to_clipboard: "In die Zwischenablage kopiert",
// Calendar
price: "Preis",
category: "Kategorie",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Σφάλμα αποστολής ειδοποίησης",
delete_account_confirmation: "Είστε σίγουρος ότι θέλετε να διαγράψετε το λογαριασμό σας;",
this_will_delete_all_data: "Αυτό θα διαγράψει όλα τα δεδομένα σας και δεν μπορεί να ανακτηθεί. Να συνεχίσω;",
success: "Επιτυχία",
copied_to_clipboard: "Αντιγράφηκε στο πρόχειρο",
// Calendar
price: "Τιμή",
category: "Κατηγορία",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Error sending notification",
delete_account_confirmation: "Are you sure you want to delete your account?",
this_will_delete_all_data: "This will delete all your data and can't be undone. Continue?",
success: "Success",
copied_to_clipboard: "Copied to clipboard",
// Calendar
price: "Price",
category: "Category",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Error al enviar la notificación",
delete_account_confirmation: "¿Estás seguro de que quieres eliminar tu cuenta?",
this_will_delete_all_data: "Esto eliminará todos tus datos y no se podrán recuperar. ¿Continuar?",
success: "Éxito",
copied_to_clipboard: "Copiado al portapapeles",
// Calendar
price: "Precio",
category: "Categoría",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Erreur lors de l'envoi de la notification",
delete_account_confirmation: "Êtes-vous sûr de vouloir supprimer votre compte ?",
this_will_delete_all_data: "Cela supprimera toutes vos données et ne pourra pas être annulé. Continuer ?",
success: "Succès",
copied_to_clipboard: "Copié dans le presse-papiers",
// Calendar
price: "Prix",
category: "Catégorie",

View File

@@ -36,6 +36,8 @@ let i18n = {
error_sending_notification: "Errore nell'invio della notifica",
delete_account_confirmation: "Sei sicuro di voler eliminare il tuo account?",
this_will_delete_all_data: "Questo eliminerà tutti i tuoi dati e non potrà essere annullato. Continuare?",
success: "Successo",
copied_to_clipboard: "Copiato negli appunti",
// Calendar
price: "Prezzo",
category: "Categoria",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "通知の送信エラー",
delete_account_confirmation: "アカウントを削除してもよろしいですか?",
this_will_delete_all_data: "これによりすべてのデータが削除され、元に戻すことはできません。続行しますか?",
success: "成功",
copied_to_clipboard: "クリップボードにコピーされました",
// Calendar
price: "価格",
category: "カテゴリ",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "알림 전송 에러",
delete_account_confirmation: "정말 계정을 삭제하시겠습니까?",
this_will_delete_all_data: "이로 인해 모든 데이터가 삭제되며 복구할 수 없습니다. 계속하시겠습니까?",
success: "성공",
copied_to_clipboard: "클립보드에 복사되었습니다",
// Calendar
price: "가격",
category: "카테고리",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Błąd wysyłania powiadomienia",
delete_account_confirmation: "Czy na pewno chcesz usunąć swoje konto?",
this_will_delete_all_data: "Spowoduje to usunięcie wszystkich danych i nie będzie można tego cofnąć. Kontynuować?",
success: "Sukces",
copied_to_clipboard: "Skopiowano do schowka",
// Calendar
price: "Cena",
category: "Kategoria",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: 'Erro ao enviar notificação',
delete_account_confirmation: "Tem a certeza de que deseja eliminar a sua conta?",
this_will_delete_all_data: "Isto irá eliminar todos os seus dados e não poderão ser recuperados. Continuar?",
success: "Sucesso",
copied_to_clipboard: "Copiado para a área de transferência",
// Calendar
price: "Preço",
category: "Categoria",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Erro ao enviar notificação",
delete_account_confirmation: "Você tem certeza que deseja excluir sua conta?",
this_will_delete_all_data: "Isso excluirá todos os seus dados e não poderão ser recuperados. Continuar?",
success: "Sucesso",
copied_to_clipboard: "Copiado para a área de transferência",
// Calendar
price: "Preço",
category: "Categoria",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Ошибка отправки уведомления",
delete_account_confirmation: "Вы уверены, что хотите удалить свою учетную запись?",
this_will_delete_all_data: "Это удалит все ваши данные и не может быть отменено. Продолжить?",
success: "Успешно",
copied_to_clipboard: "Скопировано в буфер обмена",
// Calendar
price: "Цена",
category: "Категория",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Napaka pri pošiljanju obvestila",
delete_account_confirmation: "Ali ste prepričani, da želite izbrisati svoj račun?",
this_will_delete_all_data: "To bo izbrisalo vse vaše podatke in jih ni mogoče obnoviti. Nadaljujem?",
success: "Uspeh",
copied_to_clipboard: "Kopirano v odložišče",
// Calendar
price: "Cena",
category: "Kategorija",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Грешка при слању обавештења",
delete_account_confirmation: "Да ли сте сигурни да желите да избришете свој налог?",
this_will_delete_all_data: "Ово ће избрисати све ваше податке и не може се поништити. Настави?",
success: "Успех",
copied_to_clipboard: "Копирано у привремену меморију",
// Calendar
price: "Цена",
category: "Категорија",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Greška pri slanju obaveštenja",
delete_account_confirmation: "Da li ste sigurni da želite da izbrišete svoj nalog?",
this_will_delete_all_data: "Ovo će izbrisati sve vaše podatke i ne može se poništiti. Da li nastaviti?",
success: "Uspeh",
copied_to_clipboard: "Kopirano u privremenu memoriju",
// Calendar
price: "Cena",
category: "Kategorija",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: "Bildirim gönderilirken hata oluştu",
delete_account_confirmation: "Hesabınızı silmek istediğinizden emin misiniz?",
this_will_delete_all_data: "Bu tüm verilerinizi silecek ve geri alınamaz. Devam etmek istiyor musunuz?",
success: "Başarılı",
copied_to_clipboard: "Panoya kopyalandı",
// Calendar
price: "Price",
category: "Category",

View File

@@ -35,6 +35,8 @@ let i18n = {
'error_sending_notification': '发送通知时出错',
'delete_account_confirmation': "您确定要删除您的帐户吗?",
'this_will_delete_all_data': "这将删除所有您的数据,且无法撤销。是否继续?",
'success': "成功",
'copied_to_clipboard': "已复制到剪贴板",
// Calendar
price: "价格",
category: "类别",

View File

@@ -35,6 +35,8 @@ let i18n = {
error_sending_notification: '發送通知時發生錯誤',
delete_account_confirmation: "您確定要刪除您的帳戶嗎?",
this_will_delete_all_data: "這將刪除所有資料,且無法復原。繼續?",
success: "成功",
copied_to_clipboard: "已複製到剪貼簿",
// Calendar
price: "價格",
category: "類別",

1
scripts/libs/qrcode.min.js vendored Normal file
View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because it is too large Load Diff

View File

@@ -57,6 +57,7 @@ self.addEventListener('install', function (event) {
'scripts/i18n/getlang.js',
'scripts/libs/chart.js',
'scripts/libs/sortable.min.js',
'scripts/libs/qrcode.min.js',
'images/icon/favicon.ico',
'images/icon/android-chrome-192x192.png',
'images/icon/apple-touch-icon-180',

View File

@@ -1,8 +1,14 @@
<?php
// Show all errors
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once 'includes/header.php';
?>
<script src="scripts/libs/sortable.min.js"></script>
<script src="scripts/libs/qrcode.min.js"></script>
<style>
.logo-preview:after {
content: '<?= translate('upload_logo', $i18n) ?>';
@@ -59,7 +65,8 @@ require_once 'includes/header.php';
<div class="grow">
<div class="form-group">
<label for="username"><?= translate('username', $i18n) ?>:</label>
<input type="text" id="username" name="username" value="<?= $userData['username'] ?>" disabled>
<input type="text" id="username" name="username" value="<?= $userData['username'] ?>"
disabled>
</div>
<div class="form-group">
<label for="email"><?= translate('email', $i18n) ?>:</label>
@@ -123,9 +130,114 @@ require_once 'includes/header.php';
</div>
</div>
</form>
</section>
<?php
$sql = "SELECT login_disabled FROM admin";
$stmt = $db->prepare($sql);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$loginDisabled = $row['login_disabled'];
$showTotpSection = true;
if ($loginDisabled && !$userData['totp_enabled']) {
$showTotpSection = false;
}
if ($showTotpSection) {
?>
<section class="account-section">
<header>
<h2><?= translate("two_factor_authentication", $i18n) ?></h2>
</header>
<div class="account-2fa">
<div class="form-group">
<?php
if (!$userData['totp_enabled']) {
?>
<input type="button" value="<?= translate('enable_two_factor_authentication', $i18n) ?>" id="enableTotp"
onClick="enableTotp()" />
<div class="totp-popup" id="totp-popup">
<header>
<h3><?= translate('enable_two_factor_authentication', $i18n) ?></h3>
<span class="fa-solid fa-xmark close-form" onclick="closeTotpPopup()"></span>
</header>
<div class="totp-popup-content">
<div class="totp-setup" id="totp-setup">
<div class="totp-qrcode-container">
<div id="totp-qr-code"></div>
</div>
<p class="totp-secret" id="totp-secret-code"></p>
<div class="form-group-inline">
<input type="hidden" name="totp-secret" id="totp-secret" value="" />
<input type="text" id="totp" name="totp"
placeholder="<?= translate("totp_code", $i18n) ?>" />
<input type="button" value="<?= translate('enable', $i18n) ?>" id="enableTotpButton"
onClick="submitTotp()" />
</div>
</div>
<div class="totp-setup hide" id="totp-backup-codes">
<h4><?= translate('backup_codes', $i18n) ?></h4>
<ul class="totp-backup-codes" id="backup-codes"></ul>
<div class="form-group-inline wrap">
<input type="button" class="button secondary-button grow"
value="<?= translate('copy_to_clipboard', $i18n) ?>" id="copyBackupCodes"
onClick="copyBackupCodes()" />
<input type="button" class="grow"
value="<?= translate('download_backup_codes', $i18n) ?>" id="downloadBackupCodes"
onClick="downloadBackupCodes()" />
</div>
<div class="settings-notes">
<p>
<i class="fa-solid fa-circle-info"></i>
<?= translate('totp_backup_codes_info', $i18n) ?>
</p>
</div>
</div>
</div>
</div>
<?php
} else {
?>
<input type="button" class="button secondary-button"
value="<?= translate('disable_two_factor_authentication', $i18n) ?>" id="disableTotp"
onClick="disableTotp()" />
<div class="totp-popup" id="totp-disable-popup">
<header>
<h3><?= translate('disable_two_factor_authentication', $i18n) ?></h3>
<span class="fa-solid fa-xmark close-form" onclick="closeTotpDisablePopup()"></span>
</header>
<div class="totp-popup-content">
<div class="form-group-inline">
<input type="text" id="totp-disable" name="totp-disable" placeholder="totp" />
<input type="button" value="<?= translate('disable', $i18n) ?>" id="disableTotpButton"
onClick="submitDisableTotp()" />
</div>
</div>
</div>
<?php
}
?>
</div>
<div class="settings-notes">
<p>
<i class="fa-solid fa-circle-info"></i>
<?php
if (!$userData['totp_enabled']) {
echo translate('two_factor_info', $i18n);
} else {
echo translate('two_factor_enabled_info', $i18n);
}
?>
</p>
</div>
</div>
</section>
<?php
}
?>
<section class="account-section">
<header>
<h2><?= translate('monthly_budget', $i18n) ?></h2>
@@ -140,7 +252,6 @@ require_once 'includes/header.php';
<p>
<i class="fa-solid fa-circle-info"></i> <?= translate('budget_info', $i18n) ?>
</p>
<p>
</div>
</div>
</section>
@@ -941,8 +1052,7 @@ require_once 'includes/header.php';
<div class="account-fixer">
<div class="form-group">
<input type="text" name="fixer-key" id="fixerKey" value="<?= $apiKey ?>"
placeholder="<?= translate('api_key', $i18n) ?>"
<?= $demoMode ? 'disabled title="Not available on Demo Mode"' : '' ?>>
placeholder="<?= translate('api_key', $i18n) ?>" <?= $demoMode ? 'disabled title="Not available on Demo Mode"' : '' ?>>
</div>
<div class="form-group">
<label for="fixerProvider"><?= translate('provider', $i18n) ?>:</label>

View File

@@ -28,7 +28,8 @@ svg .text-color {
.graph,
.filtermenu-content,
.subscription-main .actions,
.calendar {
.calendar,
.totp-popup {
background-color: #222;
border: 1px solid #333;
box-shadow: 0 2px 5px rgba(120, 120, 120, 0.1);
@@ -236,4 +237,15 @@ input {
.update-banner {
color: #FFF;
}
.totp-qrcode-container {
padding: 14px;
border: 1px solid #DDD;
border-radius: 8px;
}
.totp-backup-codes {
background-color: #111;
border: 2px dashed #444;
}

View File

@@ -2482,4 +2482,75 @@ input[type="radio"]:checked+label::after {
border-radius: 12px;
margin-bottom: 20px;
text-align: center;
}
.totp-popup {
display: none;
position: fixed;
width: 380px;
max-width: 90%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #FFFFFF;
border: 1px solid #EEEEEE;
border-radius: 16px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
padding: 20px;
flex-direction: column;
gap: 20px;
z-index: 2;
}
.totp-popup h3,
.totp-popup h4 {
margin: 4px 0px;
text-align: center;
}
.totp-popup.is-open {
display: flex;
}
.totp-popup-content {
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
margin-top: 20px;
}
.totp-setup {
display: flex;
flex-direction: column;
align-items: center;
}
.totp-setup.hide {
display: none;
}
.totp-qrcode-container {
padding: 14px;
border: 1px solid #333;
border-radius: 8px;
}
.totp-backup-codes {
background-color: #EEE;
border: 2px dashed #ccc;
border-radius: 8px;
padding: 10px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px 18px;
justify-content: space-evenly;
}
.totp-backup-codes li {
list-style: none;
padding: 5px 10px;
font-weight: 500;
}

208
totp.php Normal file
View File

@@ -0,0 +1,208 @@
<?php
require_once 'includes/connect.php';
require_once 'includes/checkuser.php';
require_once 'includes/i18n/languages.php';
require_once 'includes/i18n/getlang.php';
require_once 'includes/i18n/' . $lang . '.php';
require_once 'includes/version.php';
if ($userCount == 0) {
header("Location: registration.php");
exit();
}
session_start();
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
$db->close();
header("Location: .");
exit();
}
if (!isset($_SESSION['totp_user_id'])) {
$db->close();
header("Location: login.php");
exit();
}
$theme = "light";
$updateThemeSettings = false;
if (isset($_COOKIE['theme'])) {
$theme = $_COOKIE['theme'];
} else {
$updateThemeSettings = true;
}
$colorTheme = "blue";
if (isset($_COOKIE['colorTheme'])) {
$colorTheme = $_COOKIE['colorTheme'];
}
$demoMode = getenv('DEMO_MODE');
$cookieExpire = time() + (30 * 24 * 60 * 60);
$invalidTotp = false;
if (isset($_POST['one-time-code'])) {
$totp_code = $_POST['one-time-code'];
$statement = $db->prepare('SELECT totp_secret, backup_codes FROM totp WHERE user_id = :id');
$statement->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);
$result = $statement->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$totp_secret = $row['totp_secret'];
$backupCodes = json_decode($row['backup_codes'], true);
require_once 'libs/OTPHP/FactoryInterface.php';
require_once 'libs/OTPHP/Factory.php';
require_once 'libs/OTPHP/ParameterTrait.php';
require_once 'libs/OTPHP/OTPInterface.php';
require_once 'libs/OTPHP/OTP.php';
require_once 'libs/OTPHP/TOTPInterface.php';
require_once 'libs/OTPHP/TOTP.php';
require_once 'libs/Psr/Clock/ClockInterface.php';
require_once 'libs/OTPHP/InternalClock.php';
require_once 'libs/constant_time_encoding/Binary.php';
require_once 'libs/constant_time_encoding/EncoderInterface.php';
require_once 'libs/constant_time_encoding/Base32.php';
$clock = new OTPHP\InternalClock();
$totp = OTPHP\TOTP::createFromSecret($totp_secret, $clock);
$valid = $totp->verify($totp_code);
// If totp is not valid check backup codes
if (!$valid) {
if (in_array($totp_code, $backupCodes)) {
$key = array_search($totp_code, $backupCodes);
unset($backupCodes[$key]);
$backupCodes = array_values($backupCodes);
$statement = $db->prepare('UPDATE totp SET backup_codes = :backup_codes WHERE user_id = :id');
$statement->bindValue(':backup_codes', json_encode($backupCodes), SQLITE3_TEXT);
$statement->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);
$statement->execute();
$valid = true;
} else {
$invalidTotp = true;
}
} else {
$statement = $db->prepare('UPDATE totp SET last_totp_used = :last_totp_used WHERE user_id = :id');
$statement->bindValue(':last_totp_used', time(), SQLITE3_INTEGER);
$statement->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);
$statement->execute();
}
if ($valid) {
$query = "SELECT id, username, main_currency, language FROM user WHERE id = :id";
$stmt = $db->prepare($query);
$stmt->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
$_SESSION['username'] = $user['username'];
$_SESSION['loggedin'] = true;
$_SESSION['main_currency'] = $user['main_currency'];
$_SESSION['userId'] = $user['id'];
setcookie('language', $user['language'], [
'expires' => $cookieExpire,
'samesite' => 'Strict'
]);
if (!isset($_COOKIE['sortOrder'])) {
setcookie('sortOrder', 'next_payment', [
'expires' => $cookieExpire,
'samesite' => 'Strict'
]);
}
$query = "SELECT color_theme FROM settings WHERE user_id = :id";
$stmt = $db->prepare($query);
$stmt->bindValue(':id', $_SESSION['totp_user_id'], SQLITE3_INTEGER);
$result = $stmt->execute();
$settings = $result->fetchArray(SQLITE3_ASSOC);
setcookie('colorTheme', $settings['color_theme'], [
'expires' => $cookieExpire,
'samesite' => 'Strict'
]);
unset($_SESSION['totp_user_id']);
$db->close();
header("Location: .");
exit();
}
}
?>
<!DOCTYPE html>
<html dir="<?= $languages[$lang]['dir'] ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="<?= $theme == "light" ? "#FFFFFF" : "#222222" ?>" id="theme-color" />
<meta name="apple-mobile-web-app-title" content="Wallos">
<title>Wallos - Subscription Tracker</title>
<link rel="icon" type="image/png" href="images/icon/favicon.ico" sizes="16x16">
<link rel="apple-touch-icon" href="images/icon/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="images/icon/apple-touch-icon-152.png">
<link rel="apple-touch-icon" sizes="180x180" href="images/icon/apple-touch-icon-180.png">
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="styles/theme.css?<?= $version ?>">
<link rel="stylesheet" href="styles/login.css?<?= $version ?>">
<link rel="stylesheet" href="styles/themes/red.css?<?= $version ?>" id="red-theme" <?= $colorTheme != "red" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/themes/green.css?<?= $version ?>" id="green-theme" <?= $colorTheme != "green" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/themes/yellow.css?<?= $version ?>" id="yellow-theme" <?= $colorTheme != "yellow" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/themes/purple.css?<?= $version ?>" id="purple-theme" <?= $colorTheme != "purple" ? "disabled" : "" ?>>
<link rel="stylesheet" href="styles/font-awesome.min.css">
<link rel="stylesheet" href="styles/barlow.css">
<link rel="stylesheet" href="styles/login-dark-theme.css?<?= $version ?>" id="dark-theme" <?= $theme == "light" ? "disabled" : "" ?>>
<script type="text/javascript">
window.update_theme_settings = "<?= $updateThemeSettings ?>";
window.color_theme = "<?= $colorTheme ?>";
</script>
<script type="text/javascript" src="scripts/login.js?<?= $version ?>"></script>
</head>
<body class="<?= $languages[$lang]['dir'] ?>">
<div class="content">
<section class="container">
<header>
<div class="logo-image" title="Wallos - Subscription Tracker">
<?php include "images/siteicons/svg/logo.php"; ?>
</div>
<p>
<?= translate('insert_totp_code', $i18n) ?>
</p>
</header>
<form action="totp.php" method="post">
<div class="form-group">
<label for="one-time-code"><?= translate('totp_code', $i18n) ?>:</label>
<input type="text" id="one-time-code" name="one-time-code" autocomplete="one-time-code" required>
</div>
<div class="form-group">
<input type="submit" value="<?= translate('login', $i18n) ?>">
</div>
<?php
if ($invalidTotp) {
?>
<ul class="error-box">
<li>
<i class="fa-solid fa-triangle-exclamation"></i><?= translate('totp_code_incorrect', $i18n) ?>
</li>
</ul>
<?php
}
?>
</form>
</section>
</div>
</body>
</html>