MISP/app/Model/User.php

935 lines
32 KiB
PHP
Raw Normal View History

<?php
App::uses('AppModel', 'Model');
App::uses('AuthComponent', 'Controller/Component');
/**
* User Model
*
* @property Role $Role
* @property Event $Event
*/
class User extends AppModel {
/**
* Display field
*
* @var string
*/
public $displayField = 'email';
public $orgField = array('Organisation', 'name'); // TODO Audit, LogableBehaviour + org
/**
* Validation rules
*
* @var array
*/
public $validate = array(
'role_id' => array(
'numeric' => array(
'rule' => array('numeric'),
//'message' => 'Your custom message here',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'password' => array(
'minlength' => array(
'rule' => array('passwordLength'),
'message' => 'Password length requirement not met.',
//'allowEmpty' => false,
'required' => true,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
'complexity' => array(
'rule' => array('complexPassword'),
'message' => 'Password complexity requirement not met.',
//'allowEmpty' => false,
//'required' => true,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
'identical' => array(
'rule' => array('identicalFieldValues', 'confirm_password'),
'message' => 'Please re-enter your password twice so that the values match.',
//'allowEmpty' => false,
//'required' => true,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'org_id' => array(
'valueNotEmpty' => array(
'rule' => array('valueNotEmpty'),
),
2015-03-21 14:27:53 +01:00
'numeric' => array(
'rule' => array('numeric'),
2015-03-21 14:27:53 +01:00
'message' => 'The organisation ID has to be a numeric value.',
),
),
'email' => array(
'email' => array(
'rule' => array('email'),
'message' => 'Please enter a valid email address.',
//'allowEmpty' => false,
'required' => true,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
'unique' => array(
'rule' => 'isUnique',
'message' => 'An account with this email address already exists.'
),
),
'autoalert' => array(
'boolean' => array(
'rule' => array('boolean'),
//'message' => 'Your custom message here',
'allowEmpty' => true,
'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'contactalert' => array(
'boolean' => array(
'rule' => array('boolean'),
//'message' => 'Your custom message here',
'allowEmpty' => true,
'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'authkey' => array(
'minlength' => array(
'rule' => array('minlength', 40),
'message' => 'A authkey of a minimum length of 40 is required.',
'required' => true,
),
'valueNotEmpty' => array(
'rule' => array('valueNotEmpty'),
),
),
'invited_by' => array(
'numeric' => array(
'rule' => array('numeric'),
//'message' => 'Your custom message here',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'change_pw' => array(
'numeric' => array(
'rule' => array('numeric'),
//'message' => 'Your custom message here',
'allowEmpty' => true,
'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'gpgkey' => array(
'gpgvalidation' => array(
'rule' => array('validateGpgkey'),
'message' => 'GPG key not valid, please enter a valid key.',
),
),
'certif_public' => array(
'notempty' => array(
'rule' => array('validateCertificate'),
'message' => 'Certificate not valid, please enter a valid certificate (x509).',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'nids_sid' => array(
'numeric' => array(
'rule' => array('numeric'),
'message' => 'A SID should be an integer.',
'allowEmpty' => false,
'required' => true,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'termsaccepted' => array(
'boolean' => array(
'rule' => array('boolean'),
//'message' => 'Your custom message here',
//'allowEmpty' => false,
//'required' => false,
//'last' => false, // Stop validation after this rule
//'on' => 'create', // Limit validation to 'create' or 'update' operations
),
),
'newsread' => array(
'numeric' => array(
'rule' => array('numeric')
),
),
);
//The Associations below have been created with all possible keys, those that are not needed can be removed
/**
* belongsTo associations
*
* @var array
*/
public $belongsTo = array(
'Role' => array(
'className' => 'Role',
'foreignKey' => 'role_id',
'conditions' => '',
'fields' => '',
'order' => ''
2015-02-23 11:33:38 +01:00
),
'Organisation' => array(
'className' => 'Organisation',
'foreignKey' => 'org_id',
2015-02-23 11:33:38 +01:00
'conditions' => '',
'fields' => '',
'order' => ''
2015-08-03 14:12:20 +02:00
),
'Server' => array(
'className' => 'Server',
'foreignKey' => 'server_id',
'conditions' => '',
'fields' => array('Server.id', 'Server.url', 'Server.push_rules'),
2015-08-03 14:12:20 +02:00
'order' => ''
)
);
/**
* hasMany associations
*
* @var array
*/
public $hasMany = array(
'Event' => array(
'className' => 'Event',
'foreignKey' => 'user_id',
'dependent' => false,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
),
'Post'
);
public $actsAs = array(
'SysLogLogable.SysLogLogable' => array( // TODO Audit, logable
'userModel' => 'User',
'userKey' => 'user_id',
'change' => 'full',
'ignore' => array('password')
),
'Trim',
'Containable'
);
private function __generatePassword() {
$groups = array(
'0123456789',
'abcdefghijklmnopqrstuvwxyz',
'ABCDEFGHIJKLOMNOPQRSTUVWXYZ',
'!@#$%^&*()_-'
);
$passwordLength = Configure::read('Security.password_policy_length') ? Configure::read('Security.password_policy_length') : 12;
$pw = '';
for ($i = 0; $i < $passwordLength; $i++) {
$chars = implode('', $groups);
$pw .= $chars[mt_rand(0, strlen($chars)-1)];
}
foreach ($groups as &$group) {
$pw .= $group[mt_rand(0, strlen($group)-1)];
}
return $pw;
}
public function beforeValidate($options = array()) {
if (!isset($this->data['User']['id'])) {
if (isset($this->data['User']['enable_password']) && (!$this->data['User']['enable_password'] || (empty($this->data['User']['password']) && empty($this->data['User']['confirm_password'])))) {
$this->data['User']['password'] = $this->__generatePassword();
$this->data['User']['confirm_password'] = $this->data['User']['password'];
}
}
if (!isset($this->data['User']['nids_sid']) || empty($this->data['User']['nids_sid'])) $this->data['User']['nids_sid'] = mt_rand(1000000, 9999999);
return true;
}
public function beforeSave($options = array()) {
if (isset($this->data[$this->alias]['password'])) {
$this->data[$this->alias]['password'] = AuthComponent::password($this->data[$this->alias]['password']);
}
return true;
}
/**
* Checks if the GPG key is a valid key
* But also import it in the keychain.
*/
// TODO: this will NOT fail on keys that can only be used for signing but not encryption!
// the method in verifyUsers will fail in that case.
public function validateGpgkey($check) {
// LATER first remove the old gpgkey from the keychain
// empty value
if (empty($check['gpgkey'])) {
return true;
}
// we have a clean, hopefull public, key here
// key is entered
require_once 'Crypt/GPG.php';
try {
$gpg = new Crypt_GPG(array('homedir' => Configure::read('GnuPG.homedir'), 'binary' => (Configure::read('GnuPG.binary') ? Configure::read('GnuPG.binary') : '/usr/bin/gpg')));
try {
$keyImportOutput = $gpg->importKey($check['gpgkey']);
if (!empty($keyImportOutput['fingerprint'])) {
return true;
}
} catch (Exception $e) {
//debug($e);
2013-07-17 12:54:55 +02:00
$this->log($e->getMessage());
return false;
}
} catch (Exception $e) {
//debug($e);
2013-07-17 12:54:55 +02:00
$this->log($e->getMessage());
return true; // TODO was false
}
}
/**
* Checks if the certificate is a valid x509 certificate
* But also import it in the keychain.
*/
// TODO: this will NOT fail on keys that can only be used for signing but not encryption!
// the method in verifyUsers will fail in that case.
public function validateCertificate($check) {
// LATER first remove the old certif_public from the keychain
// empty value
if (empty($check['certif_public'])) {
return true;
}
// certif_public is entered
// Check if $check is a x509 certificate ?
if (openssl_x509_read($check['certif_public'])){
try {
App::uses('Folder', 'Utility');
$dir = APP . 'tmp' . DS . 'SMIME';
if (!file_exists($dir)) {
if (!mkdir($dir, 0750, true)) throw new MethodNotAllowedException('The SMIME temp directory is not writeable (app/tmp/SMIME).');
}
$msg_test = tempnam($dir, 'SMIME');
$fp = fopen($msg_test, "w");
$test = 'test';
fwrite($fp, $test);
fclose($fp);
$msg_test_encrypted = tempnam($dir, 'SMIME');
// encrypt it
if (openssl_pkcs7_encrypt($msg_test, $msg_test_encrypted, $check['certif_public'], null, 0, OPENSSL_CIPHER_AES_256_CBC)){
$parse = openssl_x509_parse($check['certif_public']);
// Valid certificate ?
$now = new DateTime("now");
$validTo_time_t_epoch = $parse['validTo_time_t'];
$validTo_time_t = new DateTime("@$validTo_time_t_epoch");
if ($validTo_time_t > $now) {
// purposes smimeencrypt ?
if (($parse['purposes'][5][0] == 1) and ($parse['purposes'][5][2] == 'smimeencrypt')){
return true;
} else {
return 'This certificate cannot be used to encrypt email';
}
} else {
return 'This certificate is expired';
}
} else {
return false;
}
} catch (Exception $e){
$this->log($e->getMessage());
}
unlink($msg_test);
unlink($msg_test_encrypted);
} else {
return false;
}
}
2016-04-04 19:23:05 +02:00
public function passwordLength($check) {
$length = Configure::read('Security.password_policy_length');
if (empty($length) || $length < 0) $length = 6;
$value = array_values($check);
$value = $value[0];
if (strlen($value) < $length) return false;
return true;
}
public function complexPassword($check) {
/*
default password:
6 characters minimum
1 or more upper-case letters
1 or more lower-case letters
1 or more digits or special characters
example: "EasyPeasy34"
If Security.password_policy_complexity is set and valid, use the regex provided.
*/
$regex = Configure::read('Security.password_policy_complexity');
if (empty($regex) || @preg_match($regex, 'test') === false) $regex = '/((?=.*\d)|(?=.*\W+))(?![\n])(?=.*[A-Z])(?=.*[a-z]).*$/';
$value = array_values($check);
$value = $value[0];
return preg_match($regex, $value);
}
public function identicalFieldValues($field=array(), $compareField=null) {
foreach ($field as $key => $value) {
$v1 = $value;
$v2 = $this->data[$this->name][$compareField];
if ($v1 !== $v2) {
return false;
} else {
continue;
}
}
return true;
}
/**
* Generates an authentication key for each user
*/
public function generateAuthKey() {
$length = 40;
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charLen = strlen($characters) - 1;
$key = '';
for ($p = 0; $p < $length; $p++) {
$key .= $characters[rand(0, $charLen)];
}
return $key;
}
2013-07-11 14:30:56 +02:00
public function generateRandomPassword() {
$length = 12;
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-+=!@#$%&*()<>/?';
$charLen = strlen($characters) - 1;
$key = '';
for ($p = 0; $p < $length; $p++) {
$key .= $characters[rand(0, $charLen)];
}
return $key;
}
public function checkAndCorrectPgps() {
$fails = array();
$users = $this->find('all', array('recursive' => 0));
foreach ($users as $user) {
if (strlen($user['User']['gpgkey']) && strpos($user['User']['gpgkey'], "\n")) {
$fails[] = $user['User']['id'] . ':' . $user['User']['id'];
//$check['gpgkey'] = trim(preg_replace('/\n', '', $check['gpgkey']));
}
}
return $fails;
}
public function getOrgs() {
2016-01-10 19:47:21 +01:00
$orgs = $this->Organisation->find('list', array(
2015-03-21 14:27:53 +01:00
'recursive' => -1,
'fields' => array('name'),
));
2016-01-10 19:47:21 +01:00
return $orgs;
}
public function getOrgMemberCount($org) {
return $this->find('count', array(
'conditions' => array(
'org =' => $org,
)));
}
public function verifyGPG($id = false) {
require_once 'Crypt/GPG.php';
$this->Behaviors->detach('Trim');
$results = array();
$conditions = array('not' => array('gpgkey' => ''));
if ($id !== false) $conditions['User.id'] = $id;
$users = $this->find('all', array(
'conditions' => $conditions,
'recursive' => -1,
));
if (empty($users)) return $results;
$currentTimestamp = time();
$gpg = new Crypt_GPG(array('homedir' => Configure::read('GnuPG.homedir'), 'binary' => (Configure::read('GnuPG.binary') ? Configure::read('GnuPG.binary') : '/usr/bin/gpg')));
foreach ($users as $k => $user) {
try {
$temp = $gpg->importKey($user['User']['gpgkey']);
debug($gpg->getSignatures());
debug($temp);
$key = $gpg->getKeys($temp['fingerprint']);
$subKeys = $key[0]->getSubKeys();
$sortedKeys = array('valid' => 0, 'expired' => 0, 'noEncrypt' => 0);
foreach ($subKeys as $subKey) {
$issue = false;
$expiration = $subKey->getExpirationDate();
if ($expiration != 0 && $currentTimestamp > $expiration) {
$sortedKeys['expired']++;
continue;
}
if (!$subKey->canEncrypt()) {
$sortedKeys['noEncrypt']++;
continue;
}
$sortedKeys['valid']++;
}
if (!$sortedKeys['valid']) {
$results[$user['User']['id']][2] = 'The user\'s PGP key does not include a valid subkey that could be used for encryption.';
if ($sortedKeys['expired']) $results[$user['User']['id']][2] .= ' Found ' . $sortedKeys['expired'] . ' subkey(s) that have expired.';
if ($sortedKeys['noEncrypt']) $results[$user['User']['id']][2] .= ' Found ' . $sortedKeys['noEncrypt'] . ' subkey(s) that are sign only.';
$results[$user['User']['id']][0] = true;
}
} catch (Exception $e){
$results[$user['User']['id']][2] = $e->getMessage();
$results[$user['User']['id']][0] = true;
}
$results[$user['User']['id']][1] = $user['User']['email'];
}
return $results;
}
2016-04-04 19:23:05 +02:00
public function verifyCertificate() {
$this->Behaviors->detach('Trim');
$results = array();
$users = $this->find('all', array(
'conditions' => array('not' => array('certif_public' => '')),
//'fields' => array('id', 'email', 'gpgkey'),
'recursive' => -1,
));
foreach ($users as $k => $user) {
$certif_public = $user['User']['certif_public'];
try{
App::uses('Folder', 'Utility');
$dir = APP . 'tmp' . DS . 'SMIME';
if (!file_exists($dir)) {
if (!mkdir($dir, 0750, true)) throw new MethodNotAllowedException('The SMIME temp directory is not writeable (app/tmp/SMIME).');
}
$msg_test = tempnam($dir, 'SMIME');
$fp = fopen($msg_test, "w");
$test = 'test';
fwrite($fp, $test);
fclose($fp);
$msg_test_encrypted = tempnam($dir, 'SMIME');
// encrypt it
if (openssl_pkcs7_encrypt($msg_test, $msg_test_encrypted, $certif_public, null, 0, OPENSSL_CIPHER_AES_256_CBC)){
$parse = openssl_x509_parse($certif_public);
// Valid certificate ?
$now = new DateTime("now");
$validTo_time_t_epoch = $parse['validTo_time_t'];
$validTo_time_t = new DateTime("@$validTo_time_t_epoch");
if ($validTo_time_t > $now){
// purposes smimeencrypt ?
if (($parse['purposes'][5][0] == 1) && ($parse['purposes'][5][2] == 'smimeencrypt')){
} else {
// openssl_pkcs7_encrypt good -- Model/User purposes is NOT GOOD'
$results[$user['User']['id']][0] = true;
}
} else {
// openssl_pkcs7_encrypt good -- Model/User expired;
$results[$user['User']['id']][0] = true;
}
} else{
// openssl_pkcs7_encrypt NOT good -- Model/User
$results[$user['User']['id']][0] = true;
}
$results[$user['User']['id']][1] = $user['User']['email'];
} catch (Exception $e){
$this->log($e->getMessage());
}
unlink($msg_test);
unlink($msg_test_encrypted);
}
return $results;
2016-04-04 19:23:05 +02:00
}
public function getPGP($id) {
$result = $this->find('first', array(
'recursive' => -1,
'fields' => array('id', 'gpgkey'),
'conditions' => array('id' => $id),
));
return $result['User']['gpgkey'];
}
2016-04-04 19:23:05 +02:00
public function getCertificate($id) {
$result = $this->find('first', array(
'recursive' => -1,
'fields' => array('id', 'certif_public'),
'conditions' => array('id' => $id),
));
return $result['User']['certif_public'];
2016-04-04 19:23:05 +02:00
}
// get the current user and rearrange it to be in the same format as in the auth component
public function getAuthUser($id) {
$conditions = array('User.id' => $id);
$user = $this->find('first', array('conditions' => $conditions, 'recursive' => -1,'contain' => array('Organisation', 'Role', 'Server')));
if (empty($user)) return $user;
// Rearrange it a bit to match the Auth object created during the login
$user['User']['Role'] = $user['Role'];
$user['User']['Organisation'] = $user['Organisation'];
$user['User']['Server'] = $user['Server'];
unset($user['Organisation'], $user['Role'], $user['Server']);
return $user['User'];
}
// get the current user and rearrange it to be in the same format as in the auth component
public function getAuthUserByUuid($id) {
$conditions = array('User.authkey' => $id);
$user = $this->find('first', array('conditions' => $conditions, 'recursive' => -1,'contain' => array('Organisation', 'Role', 'Server')));
if (empty($user)) return $user;
// Rearrange it a bit to match the Auth object created during the login
$user['User']['Role'] = $user['Role'];
$user['User']['Organisation'] = $user['Organisation'];
$user['User']['Server'] = $user['Server'];
unset($user['Organisation'], $user['Role'], $user['Server']);
2015-04-18 07:53:18 +02:00
return $user['User'];
}
public function getAuthUserByExternalAuth($id) {
$conditions = array('User.external_auth_key' => $id, 'User.external_auth_required' => true);
$user = $this->find('first', array('conditions' => $conditions, 'recursive' => -1,'contain' => array('Organisation', 'Role', 'Server')));
if (empty($user)) return $user;
// Rearrange it a bit to match the Auth object created during the login
$user['User']['Role'] = $user['Role'];
$user['User']['Organisation'] = $user['Organisation'];
$user['User']['Server'] = $user['Server'];
unset($user['Organisation'], $user['Role'], $user['Server']);
return $user['User'];
}
// Fetch all users that have access to an event / discussion for e-mailing (or maybe something else in the future.
// parameters are an array of org IDs that are owners (for an event this would be orgc and org)
public function getUsersWithAccess($owners = array(), $distribution, $sharing_group_id = 0, $userConditions = array()) {
$sgModel = ClassRegistry::init('SharingGroup');
$conditions = array();
$validOrgs = array();
$all = true;
// add owners to the conditions
if ($distribution == 0 || $distribution == 4) {
$all = false;
$validOrgs = $owners;
}
// add all orgs to the conditions that can see the SG
2015-12-18 16:27:25 +01:00
if ($distribution == 4) {
$sgOrgs = $sgModel->getOrgsWithAccess($sharing_group_id);
if ($sgOrgs === true) $all = true;
else $validOrgs = array_merge($validOrgs, $sgOrgs);
}
$validOrgs = array_unique($validOrgs);
$conditions['AND'][] = array('disabled' => 0);
if (!$all) {
$conditions['AND']['OR'][] = array('org_id' => $validOrgs);
// Add the site-admins to the list
$roles = $this->Role->find('all', array(
'conditions' => array('perm_site_admin' => 1),
'fields' => array('id')
));
$roleIDs = array();
foreach ($roles as $role) $roleIDs[] = $role['Role']['id'];
$conditions['AND']['OR'][] = array('role_id' => $roleIDs);
}
$conditions['AND'][] = $userConditions;
$users = $this->find('all', array(
'conditions' => $conditions,
'recursive' => -1,
2016-04-04 19:23:05 +02:00
'fields' => array('id', 'email', 'gpgkey', 'certif_public', 'org_id'),
'contain' => array('Role' => array('fields' => array('perm_site_admin'))),
));
foreach ($users as &$user) {
$temp = $user['User'];
unset($user['User']);
$user = array_merge($temp, $user);
}
return $users;
}
// all e-mail sending is now handled by this method
// Just pass the user ID in an array that is the target of the e-mail along with the message body and the alternate message body if the message cannot be encrypted
// the remaining two parameters are the e-mail subject and a secondary user object which will be used as the replyto address if set. If it is set and an encryption key for the replyTo user exists, then his/her public key will also be attached
2015-05-25 20:54:10 +02:00
public function sendEmail($user, $body, $bodyNoEnc = false, $subject, $replyToUser = false) {
$this->Log = ClassRegistry::init('Log');
if (Configure::read('MISP.disable_emailing')) {
$this->Log->create();
$this->Log->save(array(
'org' => 'SYSTEM',
'model' => 'User',
'model_id' => $user['User']['id'],
'email' => $user['User']['email'],
'action' => 'email',
'title' => 'Email to ' . $user['User']['email'] . ', titled "' . $subject . '" failed. Reason: Emailing is currently disabled on this instance.',
'change' => null,
));
return true;
}
if (isset($user['User']['disabled']) && $user['User']['disabled']) return true;
2015-05-25 20:54:10 +02:00
$failed = false;
$failureReason = "";
// check if the e-mail can be encrypted
2016-04-04 19:23:05 +02:00
$canEncryptGPG = false;
if (isset($user['User']['gpgkey']) && !empty($user['User']['gpgkey'])) $canEncryptGPG = true;
$canEncryptSMIME = false;
if (isset($user['User']['certif_public']) && !empty($user['User']['certif_public']) && Configure::read('SMIME.enabled')) $canEncryptSMIME = true;
2016-04-04 19:23:05 +02:00
// If bodyonlencrypted is enabled and the user has no encryption key, use the alternate body (if it exists)
if (Configure::read('GnuPG.bodyonlyencrypted') && !$canEncryptSMIME && !$canEncryptGPG && $bodyNoEnc) {
$body = $bodyNoEnc;
}
$body = str_replace('\n', PHP_EOL, $body);
2016-04-04 19:23:05 +02:00
if ($canEncryptGPG){
// Sign the body
require_once 'Crypt/GPG.php';
try {
$gpg = new Crypt_GPG(array('homedir' => Configure::read('GnuPG.homedir'), 'binary' => (Configure::read('GnuPG.binary') ? Configure::read('GnuPG.binary') : '/usr/bin/gpg'), 'debug')); // , 'debug' => true
2016-04-04 19:23:05 +02:00
$gpg->addSignKey(Configure::read('GnuPG.email'), Configure::read('GnuPG.password'));
$body = $gpg->sign($body, Crypt_GPG::SIGN_MODE_CLEAR);
} catch (Exception $e) {
$failureReason = " the message could not be signed. The following error message was returned by gpg: " . $e->getMessage();
$this->log($e->getMessage());
$failed = true;
}
2015-05-25 20:54:10 +02:00
}
2016-04-04 19:23:05 +02:00
$Email = new CakeEmail();
2015-05-25 20:54:10 +02:00
// If we cannot encrypt the mail and the server settings restricts sending unencrypted messages, return false
if (!$failed && Configure::read('GnuPG.onlyencrypted') && !$canEncryptGPG && !$canEncryptSMIME) {
2015-05-25 20:54:10 +02:00
$failed = true;
$failureReason = " encrypted messages are enforced and the message could not be encrypted for this user as no valid encryption key was found.";
}
// Let's encrypt the message if we can
2016-04-04 19:23:05 +02:00
if (!$failed && $canEncryptGPG) {
2015-05-25 20:54:10 +02:00
$keyImportOutput = $gpg->importKey($user['User']['gpgkey']);
try {
$key = $gpg->getKeys($keyImportOutput['fingerprint']);
$subKeys = $key[0]->getSubKeys();
2016-04-04 19:23:05 +02:00
$canEncryptGPG = false;
$currentTimestamp = time();
foreach ($subKeys as $subKey) {
$expiration = $subKey->getExpirationDate();
2016-04-04 19:23:05 +02:00
if (($expiration == 0 || $currentTimestamp < $expiration) && $subKey->canEncrypt()) $canEncryptGPG = true;
}
2016-04-04 19:23:05 +02:00
if ($canEncryptGPG) {
$gpg->addEncryptKey($keyImportOutput['fingerprint']); // use the key that was given in the import
$body = $gpg->encrypt($body, true);
} else {
$failed = true;
$failureReason = " the message could not be encrypted because the provided key is either expired or cannot be used for encryption.";
}
2015-05-25 20:54:10 +02:00
} catch (Exception $e){
// despite the user having a PGP key and the signing already succeeding earlier, we get an exception. This must mean that there is an issue with the user's key.
$failureReason = " the message could not be encrypted because there was an issue with the user's PGP key. The following error message was returned by gpg: " . $e->getMessage();
$this->log($e->getMessage());
$failed = true;
}
}
// SMIME if not GPG key
if (!$failed && !$canEncryptGPG && $canEncryptSMIME) {
try {
$prependedBody = 'Content-Transfer-Encoding: 7bit' . PHP_EOL . 'Content-Type: text/plain;' . PHP_EOL . ' charset=us-ascii' . PHP_EOL . PHP_EOL . $body;
App::uses('Folder', 'Utility');
$dir = APP . 'tmp' . DS . 'SMIME';
if (!file_exists($dir)) {
if (!mkdir($dir, 0750, true)) throw new MethodNotAllowedException('The SMIME temp directory is not writeable (app/tmp/SMIME).');
}
// save message to file
$msg = tempnam($dir, 'SMIME');
$fp = fopen($msg, "w");
fwrite($fp, $prependedBody);
fclose($fp);
2016-04-27 15:13:07 +02:00
$headers_smime = array("To" => $user['User']['email'], "From" => Configure::read('MISP.email'), "Subject" => $subject);
$canSign = true;
if (empty(Configure::read('SMIME.cert_public_sign')) || !is_readable(Configure::read('SMIME.cert_public_sign'))) $canSign = false;
if (empty(Configure::read('SMIME.key_sign')) || !is_readable(Configure::read('SMIME.key_sign'))) $canSign = false;
if ($canSign) {
$signed = tempnam($dir, 'SMIME');
if (openssl_pkcs7_sign($msg, $signed, 'file://'.Configure::read('SMIME.cert_public_sign'), array('file://'.Configure::read('SMIME.key_sign'), Configure::read('SMIME.password')), array(), PKCS7_TEXT)){
$fp = fopen($signed, "r");
$bodySigned = fread($fp, filesize($signed));
fclose($fp);
unlink($msg);
unlink($signed);
} else {
unlink($msg);
unlink($signed);
throw new Exception('Failed while attempting to sign the SMIME message.');
}
// save message to file
$msg_signed = tempnam($dir, 'SMIME');
$fp = fopen($msg_signed, "w");
fwrite($fp, $bodySigned);
fclose($fp);
} else {
$bodySigned = $body;
$msg_signed = $msg;
}
$msg_signed_encrypted = tempnam($dir, 'SMIME');
// encrypt it
if (openssl_pkcs7_encrypt($msg_signed, $msg_signed_encrypted, $user['User']['certif_public'], $headers_smime, 0, OPENSSL_CIPHER_AES_256_CBC)){
$fp = fopen($msg_signed_encrypted, 'r');
$bodyEncSig = fread($fp, filesize($msg_signed_encrypted));
fclose($fp);
unlink($msg_signed);
unlink($msg_signed_encrypted);
$parts = explode("\n\n", $bodyEncSig);
$bodyEncSig = $parts[1];
// SMIME transport (hardcoded headers
$Email = $Email->transport('Smime');
$body = $bodyEncSig;
} else {
unlink($msg_signed);
unlink($msg_signed_encrypted);
throw new Exception('Could not encrypt the SMIME message.');
}
} catch (Exception $e) {
// despite the user having a certificate. This must mean that there is an issue with the user's certificate.
$failureReason = " the message could not be encrypted because there was an issue with the user's public certificate. The following error message was returned by openssl: " . $e->getMessage();
$this->log($e->getMessage());
$failed = true;
2016-04-04 19:23:05 +02:00
}
}
2015-05-25 20:54:10 +02:00
$replyToLog = '';
if (!$failed) {
2016-04-04 19:23:05 +02:00
//$Email = new CakeEmail(); --> declared below (needed in the SMIME part (headers)
2015-05-25 20:54:10 +02:00
// If the e-mail is sent on behalf of a user, then we want the target user to be able to respond to the sender
// For this reason we should also attach the public key of the sender along with the message (if applicable)
if ($replyToUser != false) {
$Email->replyTo($replyToUser['User']['email']);
2016-04-04 19:23:05 +02:00
if (!empty($replyToUser['User']['gpgkey'])) {
$Email->attachments(array('gpgkey.asc' => array('data' => $replyToUser['User']['gpgkey'])));
} elseif (!empty($replyToUser['User']['certif_public'])) {
$Email->attachments(array($replyToUser['User']['email'] . '.pem' => array('data' => $replyToUser['User']['certif_public'])));
}
2015-05-25 20:54:10 +02:00
$replyToLog = 'from ' . $replyToUser['User']['email'];
}
$Email->from(Configure::read('MISP.email'));
$Email->returnPath(Configure::read('MISP.email'));
2015-05-25 20:54:10 +02:00
$Email->to($user['User']['email']);
$Email->subject($subject);
$Email->emailFormat('text');
$result = $Email->send($body);
$Email->reset();
}
$this->Log = ClassRegistry::init('Log');
$this->Log->create();
if (!$failed && $result) {
$this->Log->save(array(
'org' => 'SYSTEM',
'model' => 'User',
'model_id' => $user['User']['id'],
'email' => $user['User']['email'],
'action' => 'email',
'title' => 'Email ' . $replyToLog . ' to ' . $user['User']['email'] . ' sent, titled "' . $subject . '".',
'change' => null,
));
return true;
} else {
if (isset($result) && !$result) $failureReason = " there was an error sending the e-mail.";
$this->Log->save(array(
'org' => 'SYSTEM',
'model' => 'User',
'model_id' => $user['User']['id'],
'email' => $user['User']['email'],
'action' => 'email',
2015-05-27 17:46:01 +02:00
'title' => 'Email ' . $replyToLog . ' to ' . $user['User']['email'] . ', titled "' . $subject . '" failed. Reason: ' . $failureReason,
2015-05-25 20:54:10 +02:00
'change' => null,
));
}
return false;
}
2015-05-27 17:46:01 +02:00
public function adminMessageResolve($message) {
$resolveVars = array('$contact' => 'MISP.contact', '$org' => 'MISP.org', '$misp' => 'MISP.baseurl');
foreach ($resolveVars as $k => $v) {
$v = Configure::read($v);
$message = str_replace($k, $v, $message);
}
2015-05-27 17:46:01 +02:00
return $message;
}
public function fetchPGPKey($email) {
2015-07-13 11:05:53 +02:00
App::uses('SyncTool', 'Tools');
$syncTool = new SyncTool();
$HttpSocket = $syncTool->setupHttpSocket();
$response = $HttpSocket->get('https://pgp.mit.edu/pks/lookup?search=' . $email . '&op=index&fingerprint=on');
if ($response->code != 200) return $response->code;
$string = str_replace(array("\r", "\n"), "", $response->body);
$result = preg_match_all('/<pre>pub(.*?)<\/pre>/', $string, $matches);
$results = $this->__extractPGPInfo($matches[1]);
return $results;
}
private function __extractPGPInfo($lines) {
$extractionRules = array(
'key_id' => array('regex' => '/\">(.*?)<\/a>/', 'all' => false, 'alternate' => false),
'date' => array('regex' => '/([0-9]{4}\-[0-9]{2}\-[0-9]{2})/', 'all' => false, 'alternate' => false),
'fingerprint' => array('regex' => '/Fingerprint=(.*)$/m', 'all' => false, 'alternate' => false),
'uri' => array('regex' => '/<a href=\"(.*?)\">/', 'all' => false, 'alternate' => false),
'address' => array('regex' => '/<a href="\/pks\/lookup\?op=vindex[^>]*>([^\<]*)<\/a>(.*)Fingerprint/s', 'all' => true, 'alternate' => true),
);
$final = array();
foreach ($lines as $line) {
if (strpos($line, 'KEY REVOKED')) continue;
$temp = array();
foreach ($extractionRules as $ruleName => $rule) {
if ($rule['all']) preg_match_all($rule['regex'], $line, ${$ruleName});
else preg_match($rule['regex'], $line, ${$ruleName});
if ($rule['alternate'] && isset(${$ruleName}[2]) && trim(${$ruleName}[2][0]) != '') $temp[$ruleName] = ${$ruleName}[2];
else $temp[$ruleName] = ${$ruleName}[1];
if ($rule['all']) $temp[$ruleName] = $temp[$ruleName][0];
$temp[$ruleName] = html_entity_decode($temp[$ruleName]);
}
$temp['address'] = preg_replace('/\s{2,}/', PHP_EOL, trim($temp['address']));
$final[] = $temp;
}
return $final;
}
2016-04-22 10:52:00 +02:00
public function describeAuthFields() {
$fields = array();
$fields = array_merge($fields, array_keys($this->getColumnTypes()));
if (($key = array_search('gpgkey', $fields)) !== false) unset($fields[$key]);
if (($key = array_search('certif_public', $fields)) !== false) unset($fields[$key]);
2016-04-22 10:52:00 +02:00
$relatedModels = array_keys($this->belongsTo);
foreach ($relatedModels as $relatedModel) {
$fields[] = $relatedModel . '.*';
}
return $fields;
}
}