Enigma: Multi-host support

pull/6204/head
Aleksander Machniak 6 years ago
parent d42b51a1f3
commit 7b1f0f020b

@ -26,6 +26,7 @@ CHANGELOG Roundcube Webmail
- Composer: Fix certificate validation errors by using packagist only (#5148)
- Enigma: Add button to send mail unencrypted if no key was found (#5913)
- Enigma: Add options to set PGP cipher/digest algorithms (#5645)
- Enigma: Multi-host support
- Add --get and --extract arguments and CACHEDIR env-variable support to install-jsdeps.sh (#5882)
- Update to jquery-minicolors 2.2.6
- Support _filter and _scope as GET arguments for opening mail UI (#5825)

@ -123,6 +123,15 @@ CREATE TABLE [dbo].[searches] (
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[filestore] (
[file_id] [int] IDENTITY (1, 1) NOT NULL ,
[user_id] [int] NOT NULL ,
[filename] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
[mtime] [int] NOT NULL ,
[data] [text] COLLATE Latin1_General_CI_AI NULL ,
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[system] (
[name] [varchar] (64) COLLATE Latin1_General_CI_AI NOT NULL ,
[value] [text] COLLATE Latin1_General_CI_AI NOT NULL
@ -213,6 +222,13 @@ ALTER TABLE [dbo].[searches] WITH NOCHECK ADD
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[filestore] WITH NOCHECK ADD
CONSTRAINT [PK_filestore_file_id] PRIMARY KEY CLUSTERED
(
[file_id]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[system] WITH NOCHECK ADD
CONSTRAINT [PK_system_name] PRIMARY KEY CLUSTERED
(
@ -321,6 +337,9 @@ GO
CREATE INDEX [IX_session_changed] ON [dbo].[session]([changed]) ON [PRIMARY]
GO
CREATE INDEX [IX_filestore_user_id] ON [dbo].[filestore]([user_id]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[users] ADD
CONSTRAINT [DF_users_username] DEFAULT ('') FOR [username],
CONSTRAINT [DF_users_mail_host] DEFAULT ('') FOR [mail_host],
@ -386,6 +405,11 @@ ALTER TABLE [dbo].[searches] ADD CONSTRAINT [FK_searches_user_id]
ON DELETE CASCADE ON UPDATE CASCADE
GO
ALTER TABLE [dbo].[filestore] ADD CONSTRAINT [FK_filestore_user_id]
FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
ON DELETE CASCADE ON UPDATE CASCADE
GO
-- Use trigger instead of foreign key (#1487112)
-- "Introducing FOREIGN KEY constraint ... may cause cycles or multiple cascade paths."
CREATE TRIGGER [contact_delete_member] ON [dbo].[contacts]
@ -394,6 +418,6 @@ CREATE TRIGGER [contact_delete_member] ON [dbo].[contacts]
WHERE [contact_id] IN (SELECT [contact_id] FROM deleted)
GO
INSERT INTO [dbo].[system] ([name], [value]) VALUES ('roundcube-version', '2016112200')
INSERT INTO [dbo].[system] ([name], [value]) VALUES ('roundcube-version', '2018021600')
GO

@ -0,0 +1,24 @@
CREATE TABLE [dbo].[filestore] (
[file_id] [int] IDENTITY (1, 1) NOT NULL ,
[user_id] [int] NOT NULL ,
[filename] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
[mtime] [int] NOT NULL ,
[data] [text] COLLATE Latin1_General_CI_AI NULL ,
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[filestore] WITH NOCHECK ADD
CONSTRAINT [PK_filestore_file_id] PRIMARY KEY CLUSTERED
(
[file_id]
) ON [PRIMARY]
GO
CREATE INDEX [IX_filestore_user_id] ON [dbo].[filestore]([user_id]) ON [PRIMARY]
GO
ALTER TABLE [dbo].[filestore] ADD CONSTRAINT [FK_filestore_user_id]
FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id])
ON DELETE CASCADE ON UPDATE CASCADE
GO

@ -198,6 +198,19 @@ CREATE TABLE `searches` (
UNIQUE `uniqueness` (`user_id`, `type`, `name`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-- Table structure for table `filestore`
CREATE TABLE `filestore` (
`file_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` int(10) NOT NULL,
`filename` varchar(128) NOT NULL,
`mtime` int(10) NOT NULL,
`data` longtext NOT NULL,
PRIMARY KEY (`file_id`),
CONSTRAINT `user_id_fk_filestore` FOREIGN KEY (`user_id`)
REFERENCES `users` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE `uniqueness` (`user_id`, `filename`)
);
-- Table structure for table `system`
@ -209,4 +222,4 @@ CREATE TABLE `system` (
/*!40014 SET FOREIGN_KEY_CHECKS=1 */;
INSERT INTO system (name, value) VALUES ('roundcube-version', '2016112200');
INSERT INTO system (name, value) VALUES ('roundcube-version', '2018021600');

@ -0,0 +1,11 @@
CREATE TABLE `filestore` (
`file_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` int(10) NOT NULL,
`filename` varchar(128) NOT NULL,
`mtime` int(10) NOT NULL,
`data` longtext NOT NULL,
PRIMARY KEY (`file_id`),
CONSTRAINT `user_id_fk_filestore` FOREIGN KEY (`user_id`)
REFERENCES `users` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
UNIQUE `uniqueness` (`user_id`, `filename`)
);

@ -212,9 +212,29 @@ BEGIN
END;
/
CREATE TABLE "filestore" (
"file_id" integer PRIMARY KEY,
"user_id" integer NOT NULL
REFERENCES "users" ("user_id") ON DELETE CASCADE ON UPDATE CASCADE,
"filename" varchar(128) NOT NULL,
"mtime" integer NOT NULL,
"data" long,
CONSTRAINT "filestore_user_id_key" UNIQUE ("user_id", "filename")
);
CREATE SEQUENCE "filestore_seq"
START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE TRIGGER "filestore_seq_trig"
BEFORE INSERT ON "filestore" FOR EACH ROW
BEGIN
:NEW."user_id" := "filestore_seq".nextval;
END;
/
CREATE TABLE "system" (
"name" varchar(64) NOT NULL PRIMARY KEY,
"value" long
);
INSERT INTO "system" ("name", "value") VALUES ('roundcube-version', '2016112200');
INSERT INTO "system" ("name", "value") VALUES ('roundcube-version', '2018021600');

@ -0,0 +1,19 @@
CREATE TABLE "filestore" (
"file_id" integer PRIMARY KEY,
"user_id" integer NOT NULL
REFERENCES "users" ("user_id") ON DELETE CASCADE ON UPDATE CASCADE,
"filename" varchar(128) NOT NULL,
"mtime" integer NOT NULL,
"data" long,
CONSTRAINT "filestore_user_id_key" UNIQUE ("user_id", "filename")
);
CREATE SEQUENCE "filestore_seq"
START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE TRIGGER "filestore_seq_trig"
BEFORE INSERT ON "filestore" FOR EACH ROW
BEGIN
:NEW."user_id" := "filestore_seq".nextval;
END;
/

@ -277,6 +277,31 @@ CREATE TABLE searches (
CONSTRAINT searches_user_id_key UNIQUE (user_id, "type", name)
);
--
-- Sequence "filestore_seq"
-- Name: filestore_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--
CREATE SEQUENCE "filestore_seq"
INCREMENT BY 1
NO MAXVALUE
NO MINVALUE
CACHE 1;
--
-- Table "filestore"
-- Name: filestore; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE "filestore" (
file_id integer DEFAULT nextval('filestore_seq'::text) PRIMARY KEY,
user_id integer NOT NULL
REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
filename varchar(128) NOT NULL,
mtime integer NOT NULL,
data text NOT NULL,
CONSTRAINT filestore_user_id_filename UNIQUE (user_id, filename)
);
--
-- Table "system"
@ -288,4 +313,4 @@ CREATE TABLE "system" (
value text
);
INSERT INTO system (name, value) VALUES ('roundcube-version', '2016112200');
INSERT INTO system (name, value) VALUES ('roundcube-version', '2018021600');

@ -0,0 +1,15 @@
CREATE SEQUENCE "filestore_seq"
INCREMENT BY 1
NO MAXVALUE
NO MINVALUE
CACHE 1;
CREATE TABLE "filestore" (
file_id integer DEFAULT nextval('filestore_seq'::text) PRIMARY KEY,
user_id integer NOT NULL
REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
filename varchar(128) NOT NULL,
mtime integer NOT NULL,
data text NOT NULL,
CONSTRAINT filestore_user_id_filename UNIQUE (user_id, filename)
);

@ -191,6 +191,20 @@ CREATE TABLE cache_messages (
CREATE INDEX ix_cache_messages_expires ON cache_messages (expires);
--
-- Table structure for table filestore
--
CREATE TABLE filestore (
file_id integer PRIMARY KEY,
user_id integer NOT NULL,
filename varchar(128) NOT NULL,
mtime integer NOT NULL,
data text NOT NULL
);
CREATE UNIQUE INDEX ix_filestore_user_id ON filestore(user_id, filename);
--
-- Table structure for table system
--
@ -200,4 +214,4 @@ CREATE TABLE system (
value text NOT NULL
);
INSERT INTO system (name, value) VALUES ('roundcube-version', '2016112200');
INSERT INTO system (name, value) VALUES ('roundcube-version', '2018021600');

@ -0,0 +1,9 @@
CREATE TABLE filestore (
file_id integer PRIMARY KEY,
user_id integer NOT NULL,
filename varchar(128) NOT NULL,
mtime integer NOT NULL,
data text NOT NULL
);
CREATE UNIQUE INDEX ix_filestore_user_id ON filestore(user_id, filename);

@ -8,6 +8,8 @@ The plugin uses gpg binary on the server and stores all keys
Encryption/decryption is done server-side. So, this plugin
is for users that trust the server.
For multi-host environments see enigma_multihost setting description.
Implemented features:
---------------------
@ -36,7 +38,6 @@ TODO:
- Search filter to see invalid/expired keys
- Key server(s) support (upload, refresh)
- Mark keys as trusted/untrasted, display appropriate message in verify/decrypt status
- Support for multi-server installations (store keys in sql database? probably impossible with GnuPG 2.1)
- Performance improvements:
- cache decrypted message key id so we can skip decryption if we have no password in session
- cache (last or successful only?) sig verification status to not verify on every msg preview (optional)

@ -36,6 +36,13 @@ $config['enigma_pgp_cipher_algo'] = null;
// Run gpg --version to see the list of supported algorithms
$config['enigma_pgp_digest_algo'] = null;
// Enables multi-host environments support.
// Enable it if you have more than one HTTP server.
// Make sure all servers run the same GnuPG version and have time in sync.
// Keys will be stored in SQL database (make sure max_allowed_packet
// is big enough).
$config['enigma_multihost'] = false;
// Enables signatures verification feature.
$config['enigma_signatures'] = true;

@ -24,6 +24,8 @@ class enigma_driver_gnupg extends enigma_driver
protected $homedir;
protected $user;
protected $last_sig_algorithm;
protected $debug = false;
protected $db_files = array('pubring.gpg', 'secring.gpg');
function __construct($user)
@ -77,6 +79,7 @@ class enigma_driver_gnupg extends enigma_driver
"Unable to write to keys directory: $homedir");
}
$this->debug = $debug;
$this->homedir = $homedir;
$options = array('homedir' => $this->homedir);
@ -104,6 +107,8 @@ class enigma_driver_gnupg extends enigma_driver
catch (Exception $e) {
return $this->get_error_from_exception($e);
}
$this->db_sync();
}
/**
@ -231,10 +236,16 @@ class enigma_driver_gnupg extends enigma_driver
$this->gpg->addPassphrase($keyid, $pass);
}
if ($isfile)
return $this->gpg->importKeyFile($content);
else
return $this->gpg->importKey($content);
if ($isfile) {
$result = $this->gpg->importKeyFile($content);
}
else {
$result = $this->gpg->importKey($content);
}
$this->db_save();
return $result;
}
catch (Exception $e) {
return $this->get_error_from_exception($e);
@ -372,12 +383,14 @@ class enigma_driver_gnupg extends enigma_driver
$type = ($key->subkeys[$i]->usage & enigma_key::CAN_ENCRYPT) ? 'priv' : 'pub';
$result = $this->{'delete_' . $type . 'key'}($key->subkeys[$i]->id);
if ($result !== true) {
return $result;
break;
}
}
}
}
$this->db_save();
return $result;
}
@ -529,6 +542,147 @@ class enigma_driver_gnupg extends enigma_driver
return $ekey;
}
/**
* Syncronize keys database on multi-host setups
*/
protected function db_sync()
{
if (!$this->rc->config->get('enigma_multihost')) {
return;
}
$db = $this->rc->get_dbh();
$table = $db->table_name('filestore', true);
$result = $db->query(
"SELECT `file_id`, `filename`, `mtime` FROM $table"
. " WHERE `user_id` = ? AND `filename` IN (" . $db->array2list($this->db_files) . ")",
$this->rc->user->ID
);
while ($record = $db->fetch_assoc($result)) {
$file = $this->homedir . '/' . $record['filename'];
$mtime = @filemtime($file);
if ($mtime < $record['mtime']) {
$data_result = $db->query("SELECT `data`, `mtime` FROM $table"
. " WHERE `file_id` = ?", $record['file_id']);
$data = $db->fetch_assoc($data_result);
$data = $data ? base64_decode($data['data']) : null;
if ($data === null || $data === false) {
rcube::raise_error(array(
'code' => 605, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Enigma: Failed to sync $file ({$record['file_id']}). Decode error."
), true, false);
continue;
}
$tmpfile = $file . '.tmp';
if (file_put_contents($tmpfile, $data, LOCK_EX) === strlen($data)) {
rename($tmpfile, $file);
touch($file, $data_record['mtime']);
if ($this->debug) {
$this->debug("SYNC: Fetched file: $file");
}
}
else {
// error
@unlink($tmpfile);
rcube::raise_error(array(
'code' => 605, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Enigma: Failed to sync $file."
), true, false);
}
}
}
// No records found, do initial sync if already have the keyring
if (!$db->is_error($result) && empty($file)) {
$this->db_save(true);
}
}
/**
* Save keys database for multi-host setups
*/
protected function db_save($is_empty = false)
{
if (!$this->rc->config->get('enigma_multihost')) {
return true;
}
$db = $this->rc->get_dbh();
$table = $db->table_name('filestore', true);
$records = array();
if (!$is_empty) {
$result = $db->query(
"SELECT `file_id`, `filename`, `mtime` FROM $table"
. " WHERE `user_id` = ? AND `filename` IN (" . $db->array2list($this->db_files) . ")",
$this->rc->user->ID
);
while ($record = $db->fetch_assoc($result)) {
$records[$record['filename']] = $record;
}
}
foreach ($this->db_files as $filename) {
$file = $this->homedir . '/' . $filename;
$mtime = @filemtime($file);
if ($mtime && (empty($records[$filename]) || $mtime > $records[$filename]['mtime'])) {
$data = file_get_contents($file);
$data = base64_encode($data);
$datasize = strlen($data);
if (empty($maxsize)) {
$maxsize = min($db->get_variable('max_allowed_packet', 1048500), 4*1024*1024) - 2000;
}
if ($datasize > $maxsize) {
rcube::raise_error(array(
'code' => 605, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Enigma: Failed to save $file. Size exceeds max_allowed_packet."
), true, false);
continue;
}
if (empty($records[$filename])) {
$result = $db->query(
"INSERT INTO $table (`user_id`, `filename`, `mtime`, `data`)"
. " VALUES(?, ?, ?, ?)",
$this->rc->user->ID, $filename, $mtime, $data);
}
else {
$result = $db->query(
"UPDATE $table SET `mtime` = ?, `data` = ? WHERE `file_id` = ?",
$mtime, $data, $records[$filename]['file_id']);
}
if ($db->is_error($result)) {
rcube::raise_error(array(
'code' => 605, 'line' => __LINE__, 'file' => __FILE__,
'message' => "Enigma: Failed to save $file into database."
), true, false);
break;
}
if ($this->debug) {
$this->debug("SYNC: Pushed file: $file");
}
}
}
}
/**
* Write debug info from Crypt_GPG to logs/enigma
*/

Loading…
Cancel
Save