253 lines
6.4 KiB
PHP
253 lines
6.4 KiB
PHP
<?php
|
|
|
|
class Model {
|
|
const ENCRYPTED_PROPERTIES = [];
|
|
const PLAIN_PROPERTIES = [];
|
|
const SERVER_PROPERTIES = [];
|
|
|
|
protected $data;
|
|
protected $proofKeyKnowledge;
|
|
|
|
public function __construct() {
|
|
if (!defined('DATA_FOLDER')) {
|
|
throw new Exception('DATA_FOLDER is not defined');
|
|
}
|
|
|
|
if (!is_writable(DATA_FOLDER)) {
|
|
throw new Exception('DATA_FOLDER (' . DATA_FOLDER . ') is not writeable');
|
|
}
|
|
|
|
$this->data = new stdClass();
|
|
}
|
|
|
|
private static function convertFromStorage($string) {
|
|
return json_decode($string);
|
|
}
|
|
|
|
private static function convertToStorage($dataObj) {
|
|
$obj = clone($dataObj);
|
|
unset($obj->id);
|
|
return json_encode($obj);
|
|
}
|
|
|
|
/*
|
|
* get new object
|
|
*/
|
|
public static function create($data) {
|
|
if (!is_object($data)) {
|
|
throw new Exception('data must be an object');
|
|
}
|
|
|
|
$properties = array_merge(
|
|
static::ENCRYPTED_PROPERTIES,
|
|
static::PLAIN_PROPERTIES,
|
|
static::SERVER_PROPERTIES
|
|
);
|
|
|
|
// check if all properties exist on data object
|
|
foreach ($properties as $property) {
|
|
if (!property_exists($data, $property)) {
|
|
throw new Exception('property ' . $property . ' does not exist on data object');
|
|
}
|
|
}
|
|
|
|
// check that encrypted properties are valid AES
|
|
foreach (static::ENCRYPTED_PROPERTIES as $encryptedProperty) {
|
|
if (!self::isValidSJCL($data->$encryptedProperty)) {
|
|
throw new Exception('property ' . $property . ' is not valid AES');
|
|
}
|
|
}
|
|
|
|
$modelName = get_called_class();
|
|
$model = new $modelName;
|
|
|
|
foreach ($properties as $property) {
|
|
$model->set($property, $data->$property);
|
|
}
|
|
|
|
return $model;
|
|
}
|
|
|
|
public function export() {
|
|
$data = new stdClass();
|
|
$publicProperties = array_merge(
|
|
static::ENCRYPTED_PROPERTIES,
|
|
static::PLAIN_PROPERTIES,
|
|
array('id')
|
|
);
|
|
foreach($publicProperties as $publicProperty) {
|
|
$data->$publicProperty = $this->get($publicProperty);
|
|
}
|
|
|
|
if (method_exists($this, 'includeRelationships')) {
|
|
$this->includeRelationships($data);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
protected function get($property) {
|
|
if(property_exists($this->data, $property)) {
|
|
return $this->data->$property;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
protected function getDir() {
|
|
throw new Exception ('getDir must be implemented by model');
|
|
}
|
|
|
|
protected function getPath() {
|
|
throw new Exception ('getPath must be implemented by model');
|
|
}
|
|
|
|
/*
|
|
* Checks if a json string is a proper SJCL encrypted message.
|
|
* False if format is incorrect.
|
|
*
|
|
* Taken from: https://github.com/sebsauvage/ZeroBin/blob/8cae64d6eab99fb0d31868df77846285c0958ed0/index.php#L76-L109
|
|
* Copyright (c) 2012 Sébastien SAUVAGE (sebsauvage.net)
|
|
* License: https://github.com/sebsauvage/ZeroBin/blob/8cae64d6eab99fb0d31868df77846285c0958ed0/README.md
|
|
*/
|
|
private static function isValidSJCL($jsonstring) {
|
|
$accepted_keys=array('iv','v','iter','ks','ts','mode','adata','cipher','salt','ct');
|
|
|
|
// Make sure content is valid json
|
|
$decoded = json_decode($jsonstring);
|
|
if ($decoded==null) return false;
|
|
$decoded = (array)$decoded;
|
|
|
|
// Make sure required fields are present
|
|
foreach($accepted_keys as $k)
|
|
{
|
|
if (!array_key_exists($k,$decoded)) { return false; }
|
|
}
|
|
|
|
// Make sure some fields are base64 data
|
|
if (base64_decode($decoded['iv'],$strict=true)==null) { return false; }
|
|
if (base64_decode($decoded['salt'],$strict=true)==null) { return false; }
|
|
if (base64_decode($decoded['cipher'],$strict=true)==null) { return false; }
|
|
|
|
// Make sure no additionnal keys were added.
|
|
if (count(array_intersect(array_keys($decoded),$accepted_keys))!=10) { return false; }
|
|
|
|
// Reject data if entropy is too low
|
|
$ct = base64_decode($decoded['ct'], $strict=true);
|
|
if (strlen($ct) > strlen(gzdeflate($ct))) return false;
|
|
|
|
// Make sure some fields have a reasonable size.
|
|
if (strlen($decoded['iv'])>24) return false;
|
|
if (strlen($decoded['salt'])>14) return false;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* restore object from storage
|
|
*/
|
|
public static function restore($id) {
|
|
if (!static::isValidId($id)) {
|
|
throw new Exception($id . ' is not a valid id');
|
|
}
|
|
|
|
$modelName = get_called_class();
|
|
$model = new $modelName;
|
|
|
|
$model->set('id', $id);
|
|
$path = $model->getPath();
|
|
|
|
if (!is_file($path)) {
|
|
// no poll with this id
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$storageObject = file_get_contents(
|
|
$path
|
|
);
|
|
|
|
if ($storageObject === false) {
|
|
// no poll with this id
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception $e) {
|
|
return false;
|
|
}
|
|
|
|
$data = self::convertFromStorage($storageObject);
|
|
$properties = array_merge(
|
|
static::ENCRYPTED_PROPERTIES,
|
|
static::PLAIN_PROPERTIES,
|
|
static::SERVER_PROPERTIES
|
|
);
|
|
foreach ($properties as $property) {
|
|
if (isset($data->$property)) {
|
|
$model->set($property, $data->$property);
|
|
}
|
|
}
|
|
|
|
if (method_exists($model, 'restoreHook')) {
|
|
if ($model->restoreHook() === false) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $model;
|
|
}
|
|
|
|
/*
|
|
* save object to storage
|
|
* gives back new id
|
|
*/
|
|
public function save() {
|
|
// create dir for data if it does not exists
|
|
$counter = 0;
|
|
while (true) {
|
|
$this->set('id', $this->generateNewId());
|
|
|
|
try {
|
|
if (!is_dir($this->getDir())) {
|
|
if (mkdir($this->getDir()) === false) {
|
|
throw new Exception('could not create the directory ' . $this->getDir() . ' for data object');
|
|
}
|
|
}
|
|
|
|
if (!is_writable($this->getDir())) {
|
|
throw new Exception('directory ' . $this->getDir() . ' is not writeable');
|
|
}
|
|
|
|
// save data
|
|
if(
|
|
file_put_contents(
|
|
$this->getPath(),
|
|
self::convertToStorage($this->data),
|
|
LOCK_EX
|
|
) === false
|
|
) {
|
|
throw new Exception('Could not write data to ' . $this->getPath());
|
|
}
|
|
}
|
|
catch (Exception $e) {
|
|
if ($counter > 5) {
|
|
throw new Exception(
|
|
'write failed more than five times; last path was ' . $this->getPath(),
|
|
0,
|
|
$e
|
|
);
|
|
}
|
|
|
|
$counter++;
|
|
continue;
|
|
}
|
|
|
|
// successfully run
|
|
break;
|
|
}
|
|
}
|
|
|
|
private function set($key, $value) {
|
|
$this->data->$key = $value;
|
|
}
|
|
}
|