<?php
// (Shirai062): RSSリンクアドレスの脆弱性に対する拡張 (2008/03/28)
//
// （現状の問題点）
// ある程度のスキルを持つ者であれば自分の登録されていないコースどころか，
// 自分がアカウントを持っていないMoodleサイトのRSSであっても，RSSで公開されているならば読めてしまう．
//
// （解決法）
// RSSのリンクアドレスのデータ部を暗号化する．暗号化はサイト管理者が設定ファイルに設定した秘密鍵を用いる．
// 暗号化／復号化はTripleDES同様に２個（あるいは３個）の鍵で３回の暗号化／復号化を行う．
//
// （解決法の限界）
// RSSフィード時にユーザ認証を行っている訳ではないので，暗号化されたRSSリンクアドレスを第３者が入手すれば，
// 現状と同じくRSSフィードを受信できる．
// とはいえ，２個の秘密鍵を知らない限りは，暗号化されたRSSリンクデータを推測するのは難しい．
// さらに，登録キーを設定できるコース内のフォーラムのRSSならば，登録キーを変更することで，
// そのコースのRSSリンクアドレスが全て変更になるので，同じコースを使い回している場合であっても，
// 古いユーザに新しいユーザたちのRSSを盗み見られるのを防ぐことができる．
//
// （RSSリンクアドレスの暗号化に関する説明）
//
// 旧アドレス形式           　：暗号化を行わない生データが露出した形式（オリジナル）
// 共通鍵暗号化アドレス形式 　：サイト共通のRSS共通秘密鍵A/Bを用いてRSSリンクデータを暗号化（'/rss.xml'を取り除く）
// 　　　　　　　　　　　　　 ：以下，共通鍵アドレス形式と呼ぶ．
// コース別暗号化アドレス形式 ：RSS共通秘密鍵A/Bに加えてコースの登録キーも用いて暗号化（'/rss.xml'を取り除く）
//　                          　以下，コース別アドレス形式と呼ぶ．
// どのアドレス形式を選択するかはサイト全体で統一的に設定し，コース毎では指定できないものとする．
//
// （共通鍵アドレス形式の暗号化手順）
// (1) RSS共通秘密鍵Aで暗号化
// (2) RSS共通秘密鍵Bで復号化
// (3) RSS共通秘密鍵Aで暗号化
// （共通鍵アドレス形式の復号化手順）
// (1) RSS共通秘密鍵Aで復号化
// (2) RSS共通秘密鍵Bで暗号化
// (3) RSS共通秘密鍵Aで復号化
//
// （コース別アドレス形式の暗号化手順）
// (1) RSS共通秘密鍵Aで暗号化
// (2) RSS共通秘密鍵Bで復号化
// (3) 登録キーで暗号化
// （コース別アドレス形式の復号化手順）
// (1) 登録キーで復号化
// (2) RSS共通秘密鍵Bで暗号化
// (3) RSS共通秘密鍵Aで復号化
// サイトの指定が”コース別アドレス形式”となっていても，登録キーが登録されていないコースや
// ブログのRSSリンクアドレスの暗号化/復号化には”共通鍵アドレス形式”を用いる．
//
// コースIDの暗号化/復号化には，RSS共通秘密鍵A/Bを破られるのを防ぐために今まで同様，Moodle共通鍵を利用する．
// （コースIDは判読されても大きな問題にはならない）
// 登録キーは暗号化の切り札ではない．学生に周知しているので世の中に広く知られているものと考える．
// 登録キーを暗号化に利用する目的は安全性ではなく，特定のコースのRSSアドレスのみを変更できるようにする点にある．
// とはいえ第３者に簡単に登録キーを解読されると問題なので２個の秘密鍵による三重の暗号化を用いた．
//
//  (8) [RSSリンクアドレスのセキュリティレベル]
//  $fsCFG->rss_secure_level = 0;   // 旧アドレス形式を出力．
                                    // 全アドレス形式を受け入れる（オリジナルに近い，安全性最低）
//  $fsCFG->rss_secure_level = 1;   // 共通鍵アドレス形式を出力
                                    // 全アドレス形式を受け入れる（過渡期向け，安全性低い）
//  $fsCFG->rss_secure_level = 2;   // コース別暗号化形式を出力
                                    // 全アドレス形式を受け入れる（過渡期向け，安全性低い）
//  $fsCFG->rss_secure_level = 3;   // 共通鍵アドレス形式を出力
                                    // 暗号化アドレス形式（共通鍵／コース別）のみ受け入れる（過渡期向け，安全性中）
//  $fsCFG->rss_secure_level = 4;   // コース別アドレス形式を出力
                                    // 暗号化アドレス形式（共通鍵／コース別）のみ受け入れる（安全性高い）
//  $fsCFG->rss_secure_level = 5;   // コース別アドレス形式を出力
                                    // コース別アドレス形式のみ受け入れる（安全性最高）
//
//  (9) [RSS共通秘密鍵A/B]
//  $fsCFG->rssCryptKeyA = 'honyarara';
//  $fsCFG->rssCryptKeyB = 'nanjamonja';
// これら二つの変数は必ず設定して下さい．マルチバイト文字の使用が安全かどうかは保障できません．
// RSS共通秘密鍵AとBには特に重要性の違いは無い．
//
// ＜'/rss_xml'を暗号化時に取り除く理由＞
// (1) データ量削減
// 　blogもforumもRSSリンクアドレスの末尾はrss.xmlで共通なので暗号化時には取り除く．
// (2) 暗号の信頼性向上
// 　Moodleのrss/file.phpでは，利便性の優先か，末尾のrss.xmlが一部欠けていても受け入れるコードになっている．
// 加えて，rc4encrypt()/rc4decrypt()の暗号化はあまり高度な暗号化ではないため，末尾の欠けた暗号を復号化しても
// 末尾以外は復号化できてしまう．１文字異なるだけで全体の値が大きく変わるタイプの暗号ではない．
//
// ＜登録キーを用いた暗号化の安全性＞
// 　登録キー自体をRSSリンクアドレスには含めていないのでご安心を．
// ただし何も情報が存在しないと，復号化の際に必要な解読キー（コースの登録キー）を得られない．
// そこでコースIDを暗号に付け加えている（Moodle共通の暗号化鍵で暗号化／復号化）．
//
// ＜コース別アドレス形式の暗号化／復号化手順の詳細説明＞
//
// （暗号化時）
// 　・RSSリンクデータの末尾の'/rss.xml'を取り除く．
// 　・RSS共通秘密鍵Aで暗号化，
// 　・RSS共通秘密鍵Bで復号化，
// 　・コースの登録キーで暗号化して暗号化されたRSSリンクデータを得る(DATAとする)．
//     （コースの登録キーは変数$course->passwordで得られる）
// 　・コースID（自然数）をMoodle共通の暗号化鍵で暗号化（IDとする）．
// 　・IDとDATAを'_'（アンダーバー）で連結して，コース別アドレス形式のRSSリンクデータを得る（ID1_DATA2）．
// 　復号化の際には，暗号化されたアドレス形式（2桁の１６進表現文字列の羅列）中に１個の'_'が存在すれば，
// 　コース別アドレス形式であると識別できる．
//
// （復号化時）
// 　・暗号化されたRSSリンクデータを暗号化されたコースIDとコース登録キーも用いて暗号化されたリンクデータに分離する．
// 　・Moodle共通の暗号化鍵で復号化してコースID（自然数）を得る．
// 　・コースIDを用いてデータベースよりコースの登録キーを取得する．
// 　　もしコースが存在しない場合はNG．あるいは登録キーが存在しないならば復号化できないのでNG．
// 　・リンクデータをコースの登録キーで復号化し，
//   ・RSS共通秘密鍵Bで暗号化し，
//   ・RSS共通秘密鍵Aで復号化し，末尾に'/rss.xml'を付加してRSSリンクアドレスを得る．
//
// 　もしRSSが盗み見られている兆候があった場合は以下，いずれかの対策を行う．
// 　(1)RSS共通秘密鍵A/Bいずれかを変更する：サイト内の全てのRSSリンクアドレスが変更される．
// 　(2)コースの登録キーを変更する　　    ：そのコース内のフォーラムのRSSリンクアドレスのみ変更される．
// RSSリンクアドレスが変更されると，古いRSSリンクアレスではRSSフィードを読み出せなくなる．．
// Moodle共通の暗号化鍵（コースIDの暗号化に利用）を変更すると，RSSリンクアドレス以外の機能に関わる暗号化も
// 影響を受ける恐れがある．そこでrc4encrypt(), rc4decrypt()をコピーしてrc4encrypt_rss(), rc4decrypt_rss()としている．
// $fsCFG->rssCryptKeyA/Bはいまのところ本件にしか利用していないので，他の機能に影響を与えない．．
//

// バイナリーデータ(string）を２桁の１６進表現文字列に変換する関数
function bin2rawHex($str)
{
    $hexString = '';
    for ($i = 0; $i < strlen($str); $i++) {
        $hexString .= sprintf("%02X", ord(substr($str, $i, 1)));
    }
    return $hexString;
}
// ２桁の１６進表現文字列をバイナリーデータ(string)に戻す関数
function rawHex2bin($str)
{
    $binData = '';
    for ($i = 0; $i < strlen($str) / 2; $i++) {
        sscanf(substr($str, $i * 2, 2), "%02X", $bbyte);
        $binData .= chr($bbyte);
    }
    return $binData;
}

// RSSリンクのデータを暗号化（2桁の16進表現文字列に変換）
// $password省略時は秘密キーを使用（cf. rc4encrypt_rss())
function rss_link_encrypt_triple($str, $password1 = false, $password2 = false, $password3 = false) {
    // 暗号化
    $encrypted =           rc4encrypt_rss($str,       $password1);
// (2009/06/19)これは間違い．特定の組み合わせで不具合が発生する．修正しても副作用は無い．
//  $encrypted = rawurldecode(rc4decrypt_rss($encrypted, $password2));
    $encrypted =           rc4decrypt_rss($encrypted, $password2);
    $encrypted = urldecode(rc4encrypt_rss($encrypted, $password3));
    // 文字列化
    $hexString = bin2rawHex($encrypted);
    return $hexString;
}
// 暗号化されたRSSリンクのデータ（2桁の16進表現文字列）を復号化
// $password省略時は秘密キーを使用（cf. rc4decrypt_rss())
function rss_link_decrypt_triple($str, $password1 = false, $password2 = false, $password3 = false) {
    // バイナリー化
    $binString = rawHex2bin($str);
    // 復号化
    $decrypted = rc4decrypt_rss(urlencode($binString), $password1);
    $decrypted = rc4encrypt_rss(          $decrypted , $password2);
    $decrypted = rc4decrypt_rss(          $decrypted , $password3);
    return $decrypted;
}

// 以下の２つの暗号化／復号化関数はlib/moodlelib.php中のrc4encrypt()とrc4decrypt()を改造したもの

define('RC4CRYPTKEY_RSS', 'dwxlwia');

/**
 * rc4encrypt_rss
 *
 * @param string $data ?
 * @return string
 */
// 暗号化
function rc4encrypt_rss($data, $password = false) {
    if (empty($password)) $password = RC4CRYPTKEY_RSS;
    return endecrypt($password, $data, '');
}
/**
 * rc4decrypt_rss
 *
 * @param string $data ?
 * @return string
 */
// 復号化
function rc4decrypt_rss($data, $password = false) {
    if (empty($password)) $password = RC4CRYPTKEY_RSS;
    return endecrypt($password, $data, 'de');
}

// RSSリンクデータを作成するメインの関数
// RSSリンクアドレスデータの形式
//  Forum : "$courseid/$userid/$modulename/$id/rss.xml";
//  Blog  : SITEID."/$userid/blog/site/".SITEID.'/rss.xml';             ('site')
//          "$filterselect/$userid/blog/course/$filterselect/rss.xml";  ('course')
//          SITEID."/$userid/blog/group/$filterselect/rss.xml";         ('group')
//          SITEID."/$userid/blog/user/$filterselect/rss.xml";          ('user')
// ただし，暗号化する場合は'/rss.xml'を付けない．復号化後に付加する．
function generate_secure_rss_link($rss_link, $modulename) {
    global $CFG;
    global $fsCFG;
    global $course;

    // rss_secure_levelに応じた暗号化処理
    switch ($fsCFG->rss_secure_level) {
        // Level-1 & 3：RSS共通秘密鍵A/Bによる暗号化のみ 
      case 1:
      case 3:
        $key1 = $fsCFG->rssCryptKeyA;
        $key2 = $fsCFG->rssCryptKeyB;
        $key3 = $fsCFG->rssCryptKeyA;
        $rss_linkdata = rss_link_encrypt_triple($rss_link, $key1, $key2, $key3);
        break;
        // Level-2/4/5：RSS共通秘密鍵A/Bと登録キーによるコース別アドレス形式の暗号化
      case 2:
      case 4:
      case 5:
        $key1 = $fsCFG->rssCryptKeyA;
        $key2 = $fsCFG->rssCryptKeyB;
        $key3 = $fsCFG->rssCryptKeyA;   // 一時的に．コース登録キーが無い場合はRSS共通鍵アドレス形式と等価になる．
        if ($modulename == 'blog') {
            // blogには登録キーが無いので一重の暗号化（Level-1/Level2同等）
            $rss_linkdata = rss_link_encrypt_triple($rss_link, $key1, $key2, $key3);
        } else {
            // それ以外（=Forum）の場合
            if ($course->password != '') {
                // 登録キーが存在する場合
                $key3 = (string)$course->password;
                $rss_linkdata = rss_link_encrypt_triple($rss_link, $key1, $key2, $key3);
                // 登録キーを読み出すために必要なコースIDを付加．
                // RSS共通秘密鍵を推測されないようにMoodleのキーで暗号化
                $cid = bin2rawHex(urldecode(rc4encrypt($course->id)));
                $rss_linkdata = $cid.'_'.$rss_linkdata; 
            } else {
                // 登録キーが存在しない場合はRSS共通鍵アドレス形式の暗号化（Level-1/Level-2同等）
                $rss_linkdata = rss_link_encrypt_triple($rss_link, $key1, $key2, $key3);
            }
        }
        break;
        // Level-0，あるいはrss_secure_levelが設定されていない場合は暗号化しない．
      case 0:
      default:
        $rss_linkdata = $rss_link.'/rss.xml';
        break;
    }

    // フルパスを作成
    require_once($CFG->libdir.'/filelib.php');
    if (function_exists('get_file_url')) {  // Moodle1.9.2以降対応
        $rsspath = get_file_url($rss_linkdata, null, 'rssfile');
    } else {
        if ($CFG->slasharguments) {
            $rsspath = "$CFG->wwwroot/rss/file.php/".$rss_linkdata;
        } else {
            $rsspath = "$CFG->wwwroot/rss/file.php?file=/".$rss_linkdata;
        }
    }
    return $rsspath;
}

// RSSリンクを解析し，暗号化されているならば復号化を試みる．
// 成功した場合はパスを，失敗した場合は false を返す．
function analyze_secure_rss_link($relativepath) {
    global $fsCFG;

    $rss_linkdata = trim($relativepath, '/');
    // RSSリンクデータの形式を解析
    if (strpos($rss_linkdata, '/') === false) {
        // 暗号化されている場合 (全Levelで受入れの可能性）
        $key1 = $fsCFG->rssCryptKeyA;   // 一時的に．コース登録キーが無い場合はRSS共通鍵アドレス形式と等価になる．
        $key2 = $fsCFG->rssCryptKeyB;
        $key3 = $fsCFG->rssCryptKeyA;
        if (strpos($rss_linkdata, '_') !== false) {
            // 登録キーを含む場合（つまりblogではない）
            // 暗号化されたコースIDとRSSリンクデータの２つで構成されているはず．
            if (count($args = explode('_', $rss_linkdata)) != 2) return false;
            // データベースから現在の登録キーを呼び出すために，コースIDをMoodle共通の秘密キーで復号化．
            $courseid = clean_param($args[0], PARAM_FILE);
            $courseid = (int)rc4decrypt(urlencode(rawHex2bin($courseid)));
            // もしそのようなコースIDのコースが存在しないならばNG．
            if (!$course = fs_get_record('course', 'id', $courseid)) return false;
            // もしそのコースに登録キーが存在しないならば復号化できないのでNG.
            if ($course->password == '') return false;
            // 得られた登録キーで復号化を試みる．
            $key1 = $course->password;
            $rss_linkdata = $args[1];
            $relativepath = rss_link_decrypt_triple($rss_linkdata, $key1, $key2, $key3).'/rss.xml';
            // 検査
            // a) 文字化けは無いか？
            if (ereg('[^0-9^a-z^A-Z/\.]', $relativepath)) return false;
            // b) スラッシュ記号で区切られた形式か？
            if (count(explode('/', trim($relativepath, '/'))) < 5) return false;
        } else {
            // 登録キーを含まない場合（ブログの場合もこちら）
            $relativepath = rss_link_decrypt_triple($rss_linkdata, $key1, $key2, $key3).'/rss.xml';
            // 検査
            // a) 文字化けは無いか？
            if (ereg('[^0-9^a-z^A-Z/\.]', $relativepath)) return false;
            // b) スラッシュ記号で区切られた形式か？
            if (count($args = explode('/', trim($relativepath, '/'))) < 5) return false;
            // c) Level-5かつコースに登録キーが登録されている場合はNG（ブログは除く）
            if ($fsCFG->rss_secure_level == 5) {
                if (clean_param($args[2], PARAM_FILE) !== 'blog') {
                    // ブログではない場合
                    $courseid = (int)clean_param($args[0], PARAM_FILE);
                    // もしそのようなコースIDのコースが存在しないならばNG．
                    if (!$course = fs_get_record('course', 'id', $courseid)) return false;
                    // もし登録キーが存在するならばNG（登録キーによるコース別アドレス形式のはず）
                    if ($course->password != '') return false;
                }
            }
        }
        return $relativepath;
    } else {
        // まったく暗号化されていない場合 (Level-0,Level-1,Level-2以外は受け入れてはいけない）
        if (($fsCFG->rss_secure_level != 0) &&
            ($fsCFG->rss_secure_level != 1) && ($fsCFG->rss_secure_level != 2)) return false;
        return $relativepath;
    }
    return false;
}

?>