воскресенье, 31 января 2010 г.

Vkontakte: безопасная передача данных от клиента к серверу в iFrame приложениях

Не так давно Vkontakte ввело возможность разработки iFrame приложений. В связи с этим у многих разработчиков встал вопрос как защитить данные, передаваемые между приложением и api.

Я бы хотел предложить свое решение:
Защиту данных можно разделить на следующие аспекты:

1. Подтверждение информации о пользователе.
Чтобы получить достоверные подписанные данные о пользователе, необходимо использовать первый запрос к API в настройках приложения.
method=getProfiles&uids={viewer_id}&format=json&v=2.0
Тем самым мы получаем несколько параметров, один из которых нужный нам - viewer_id, а также сигнатуру данных (auth_key), подписанных секретным ключом, который гарантирует нам верность данных.
Т.е. на следующих этапах мы можем использовать информацию о пользователе, не боясь что данные были подменены.

2. Передача данных от пользователя к приложению
Чтобы скрыть секретный ключ и при этом иметь возможность передать данные на сервер придется использовать промежуточное звено - наш ajax обработчик.
Клиентский JavaScript:
function secureSend(_data, _callback) {
  $.ajax({
   type: "POST",
   url: "ajax.php",
   data: (_data),
   dataType: "json",
   success: function(data) {
     if(data.error != undefined) {
        error(data.error);
        return false;
     } else {
      _callback(data.response);
     }
   }
 });
}
На сервере же расположить скрипт обрабатывающий только обращения к API, которые пересылают данные от пользователя к серверу. Разрешить можно все действия, т.к. пользователя мы проверили на предыдущем этапе и уже тут он волен сколько угодно раз пересылать свои голоса к приложению :)
ajax.php, серверный скрипт
$vk_allow_methods = array("secure.withdrawVotes", "secure.getBalance");
$q = $_POST;
$d = "";
if(in_array($q['method'], $vk_allow_methods)) {
  $d = vkSend($q['method']);
}

function vkSend($method) {
  $server = "http://api.vkontakte.ru/api.php?";
  $key = "000";
  $aid = 1;
  
  $qr = array();
  $qr[] = 'method='.$method;
  $qr[] = 'api_id='.$aid;
  $qr[] = 'v=2.0';
  $qr[] = 'format=JSON';
  $qr[] = 'timestamp='.time();
  $qr[] = 'random='.rand(1, 999999);
  $qr[] = 'test_mode=1';
  sort($qr);
  
  $url4sign = implode("", $qr).$key;
  $sign     = md5($url4sign);
  $url      = implode("&", $qr)."&sig=".$sign;
  
  return file_get_contents($server.$url);
}

echo $d;

3. Передача данных от приложения к пользователю.
Такие события не должны ни в коем случае инициироваться клиентским кодом. Если необходимо перевести голоса со счета приложения на счет пользователя, то лучше воспользоваться запароленым скриптом, вызывающимся по cron или вручную администратором.

суббота, 9 января 2010 г.

Flex, PHP: Безопасная передача данных с клиента на сервер

В ходе построения клиент-серверного приложения, требуется реализовать возможность защищенной передачи данных.
В этом случае я использую следующий подход:

1. Подписывание данных секретным ключом.
Это дает гарантию, что данные не будут подменены на промежуточных узлах до сервера.
Технология следующая:

a. Генерируем секретный ключ, о котором знает только приложения клиента и сервера
b. К передаваемым данным на сервер добавляем еще один параметр равный хэшу данных с секретным ключом. Пример на Flex:
var key:String = "0000-0000-0000-0000-0000";
var sreq:String = "method=setBalance&addMoney=1000"; //сам запрос
//отсортируем параметры
//чтобы последовательность параметром и на клиенте и на сервере была одинакова. Это гарантирует схождение хэшей.
sreq = sreq.split("&").sort().join("&");
//добавим к параметрам время, чтобы нельзя было продублировать запрос 
var now:Date = new Date();
var sec = now.getTime().toString().substr(0, -3);
//конечный запрос к серверу с дополнительным параметром = хэшу запроса и секретного ключа
sreq = sreq + "&timestamp="+sec+"&sig="+MD5.hash(sreq+key);
c. Сервер принимает запрос и проверяет данные используя свой секретный ключ.
К сожалению нельзя использовать четкое совпадение времени клиента и сервера, т.к. оно может отличаться или сообщение может идти до нескольких секунд. Из-за этого на сервере допускается расхождение timestamp до 100 секунд. Серверный код на PHP:
$_TIME    = $_SERVER['REQUEST_TIME'];//время запроса
$_KEY     = "0000-0000-0000-0000-0000";//тотже ключ, что и на клиенте
$_TIMEOUT = 100; //допустимое отклонение времени
// Проверим валидность данных
//присланная подпись должна сойтись со сгенерированной на основе закрытого ключа 
$q = $_GET;
//отсортируем также как в flex
unset($q['timestamp'], $q['sig']);
ksort($q);
$req = "";
foreach($q as $k=>$v) {
  $req .= $k."=".$v."&";
}
$req = substr($req, 0, -1);

if($_GET['sig'] != md5($req.$_KEY)) {
  //подписи не сошлись. Данные были подменены.
  exit;
}
//Проверим расхождение времени
if($_GET['timestamp'] + $_TIMEOUT < $_TIME) {
  //Расхождение более чем на 100 секунд. Прерываем запрос
  exit;
}

К сожалению такой метод не гарантирует нам, что запрос не будет перехвачен на промежуточном узле и не продублирован N раз в течении этих 100 секунд. Для устранения этого служит следующий пункт:

2. Проверка запроса на дублирование.
Идею я позаимствовал из TCP/IP. Добавим к запросу еще один параметр = счетчику сообщений, а на сервере будем хранить значение последнего счетчика. На клиенте достаточно сгенерировать счетчик = текущее значение + 1, а на сервере принять, проверить что новое значение больше старого и обновить серверный счетчик. Это гарантирует нам, что если злоумышленник перехватит запрос и пошлет его заново, то сервер его отвергнет, т.к. значение будет меньше хранящегося на сервере.
Для реализации этого механизма на сервере нам уже понадобится БД, а на клиенте дополнительный запрос на получение счетчика.

Под катом можно увидеть полный код клиента и сервера:

вторник, 5 января 2010 г.

Flex: Построение древовидного объекта по XML данным

Начал изучать Flex. Хочу поделиться некоторыми наработками, которые могут пригодиться.

Класс для создания древовидного объекта по XML строке.
Допустим у нас есть XML данные, полученные с удаленного хоста или еще как-нибудь. Xml может иметь смешанный тип, т.е. есть и обязательные теги (типа response), так и повторяющиеся на разных уровнях (section, id и т.д.)
Xml:

В программе удобней работать с объектом, чем с текстовыми данными, для этого служит следующий класс:
package Lib { 
  public class XMLTree {
  private var _xml:XML = null;
  private var _data:Object = new Object();
  private var _maxElem:int = 500;//ограничение на число элементов
     
  //конструктор, передается текстовый xml
  public function XMLTree(_xml:String) {
   this._xml = new XML(_xml); //создание объекта
   this.xmlParser(this._xml, this._data);//парсинг
  }
  
  public function getData():Object {
   return this._data;
  }
  
  //рекурсивный обход дерева
  private function xmlParser(node, obj:Object):void {
   var sObjName = "";
   //по каждому ответвлению
   for (var i = 0; i < node.length() && i < this._maxElem; i++) {
     sObjName = node[i].name();
     //если это ветка (имеет дочерей)
     if(node[i].hasComplexContent()) {
       //т.к. названия веток могут повторяться. Если свойство уже существует, то создается свойство с именем = имя + _ + уникальный идентификатор
       if(obj[sObjName] != undefined) {
         sObjName = sObjName + "_" + i;
       }
       //передаем ветку и текущее положение в деревянном объекте на следующий цикл рекурсии
       obj[sObjName] = new Object;
       this.xmlParser(node[i].children(), obj[sObjName]);
     } else {
       //создаем свойство с именем ветки XML объекта и текстовым значением
       obj[sObjName] = node[i].text();
     } //if
    }//for
   }//func
 }//class
}//package
Следующий код создаем объект и перечисляет значения первой ветки section дерева:
var xmlObj:XMLTree = new XMLTree(sXml);
var data:Object = xmlObj.getData();
for(var o in data.response.section) {
  Alert.show(data.response.section[o]);
}