diff --git a/common/ASC.Data.Storage/ChunkedUploader/CommonChunkedUploadSessionHolder.cs b/common/ASC.Data.Storage/ChunkedUploader/CommonChunkedUploadSessionHolder.cs index bee1df508a..9813a5ca0d 100644 --- a/common/ASC.Data.Storage/ChunkedUploader/CommonChunkedUploadSessionHolder.cs +++ b/common/ASC.Data.Storage/ChunkedUploader/CommonChunkedUploadSessionHolder.cs @@ -167,7 +167,7 @@ namespace ASC.Core.ChunkedUploader if (uploadSession.BytesTotal == uploadSession.BytesUploaded) { - return new FileStream(uploadSession.ChunksBuffer, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, + return new FileStream(uploadSession.ChunksBuffer, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, 4096, FileOptions.DeleteOnClose); } } diff --git a/common/ASC.Data.Storage/Configuration/Appender.cs b/common/ASC.Data.Storage/Configuration/Appender.cs index 3cff660614..1765c33ea0 100644 --- a/common/ASC.Data.Storage/Configuration/Appender.cs +++ b/common/ASC.Data.Storage/Configuration/Appender.cs @@ -76,6 +76,7 @@ namespace ASC.Data.Storage.Configuration public bool Public { get; set; } public bool DisableMigrate { get; set; } public bool Count { get; set; } = true; + public bool DisabledEncryption { get; set; } public IEnumerable Domain { get; set; } } diff --git a/common/ASC.Data.Storage/DiscStorage/DiscDataStore.cs b/common/ASC.Data.Storage/DiscStorage/DiscDataStore.cs index fcde26c50b..cfcb735954 100644 --- a/common/ASC.Data.Storage/DiscStorage/DiscDataStore.cs +++ b/common/ASC.Data.Storage/DiscStorage/DiscDataStore.cs @@ -32,6 +32,7 @@ using System.Linq; using ASC.Common.Logging; using ASC.Core; using ASC.Data.Storage.Configuration; +using ASC.Data.Storage.Encryption; using ASC.Security.Cryptography; using Microsoft.AspNetCore.Http; @@ -62,10 +63,16 @@ namespace ASC.Data.Storage.DiscStorage ToDictionary(x => x.Name, y => y.Expires); _domainsExpires.Add(string.Empty, moduleConfig.Expires); + + var settings = moduleConfig.DisabledEncryption ? new EncryptionSettings() : EncryptionSettings.Load(); + + Crypt = EncryptionFactory.GetCrypt(moduleConfig.Name, settings); + return this; } public DiscDataStore( + ICrypt crypt, TenantManager tenantManager, PathUtils pathUtils, EmailValidationKeyProvider emailValidationKeyProvider, @@ -73,6 +80,7 @@ namespace ASC.Data.Storage.DiscStorage IOptionsMonitor options) : base(tenantManager, pathUtils, emailValidationKeyProvider, httpContextAccessor, options) { + Crypt = crypt; } public string GetPhysicalPath(string domain, string path) @@ -93,13 +101,18 @@ namespace ASC.Data.Storage.DiscStorage } public override Stream GetReadStream(string domain, string path) + { + return GetReadStream(domain, path, true); + } + + public Stream GetReadStream(string domain, string path, bool withDecription) { if (path == null) throw new ArgumentNullException("path"); var target = GetTarget(domain, path); if (File.Exists(target)) { - return File.OpenRead(target); + return withDecription ? Crypt.GetReadStream(target) : File.OpenRead(target); } throw new FileNotFoundException("File not found", Path.GetFullPath(target)); } @@ -112,8 +125,8 @@ namespace ASC.Data.Storage.DiscStorage if (File.Exists(target)) { - var stream = File.OpenRead(target); - if (0 < offset) stream.Seek(offset, SeekOrigin.Begin); + var stream = Crypt.GetReadStream(target); + if (0 < offset && stream.CanSeek) stream.Seek(offset, SeekOrigin.Begin); return stream; } throw new FileNotFoundException("File not found", Path.GetFullPath(target)); @@ -173,6 +186,8 @@ namespace ASC.Data.Storage.DiscStorage QuotaUsedAdd(domain, fslen); + Crypt.EncryptFile(target); + return GetUri(domain, path); } @@ -211,11 +226,21 @@ namespace ASC.Data.Storage.DiscStorage public override Uri FinalizeChunkedUpload(string domain, string path, string uploadId, Dictionary eTags) { + var target = GetTarget(domain, path); + if (QuotaController != null) { - var size = GetFileSize(domain, path); + if (!File.Exists(target)) + { + throw new FileNotFoundException("file not found " + target); + } + + var size = new FileInfo(target).Length; QuotaUsedAdd(domain, size); } + + Crypt.EncryptFile(target); + return GetUri(domain, path); } @@ -233,6 +258,8 @@ namespace ASC.Data.Storage.DiscStorage get { return true; } } + private ICrypt Crypt { get; set; } + #endregion public override void Delete(string domain, string path) @@ -242,7 +269,7 @@ namespace ASC.Data.Storage.DiscStorage if (File.Exists(target)) { - var size = new FileInfo(target).Length; + var size = Crypt.GetFileSize(target); File.Delete(target); QuotaUsedDelete(domain, size); @@ -264,7 +291,7 @@ namespace ASC.Data.Storage.DiscStorage if (!File.Exists(target)) continue; - var size = new FileInfo(target).Length; + var size = Crypt.GetFileSize(target); File.Delete(target); QuotaUsedDelete(domain, size); @@ -282,7 +309,7 @@ namespace ASC.Data.Storage.DiscStorage var entries = Directory.GetFiles(targetDir, pattern, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); foreach (var entry in entries) { - var size = new FileInfo(entry).Length; + var size = Crypt.GetFileSize(entry); File.Delete(entry); QuotaUsedDelete(domain, size); } @@ -307,8 +334,9 @@ namespace ASC.Data.Storage.DiscStorage var fileInfo = new FileInfo(entry); if (fileInfo.LastWriteTime >= fromDate && fileInfo.LastWriteTime <= toDate) { + var size = Crypt.GetFileSize(entry); File.Delete(entry); - QuotaUsedDelete(domain, fileInfo.Length); + QuotaUsedDelete(domain, size); } } } @@ -345,7 +373,7 @@ namespace ASC.Data.Storage.DiscStorage Directory.CreateDirectory(Path.GetDirectoryName(newtarget)); } - var flength = new FileInfo(target).Length; + var flength = Crypt.GetFileSize(target); //Delete file if exists if (File.Exists(newtarget)) @@ -394,7 +422,7 @@ namespace ASC.Data.Storage.DiscStorage if (!Directory.Exists(targetDir)) return; var entries = Directory.GetFiles(targetDir, "*.*", SearchOption.AllDirectories); - var size = entries.Select(entry => new FileInfo(entry)).Select(info => info.Length).Sum(); + var size = entries.Select(entry => Crypt.GetFileSize(entry)).Sum(); var subDirs = Directory.GetDirectories(targetDir, "*", SearchOption.AllDirectories).ToList(); subDirs.Reverse(); @@ -411,7 +439,7 @@ namespace ASC.Data.Storage.DiscStorage if (File.Exists(target)) { - return new FileInfo(target).Length; + return Crypt.GetFileSize(target); } throw new FileNotFoundException("file not found " + target); } @@ -423,8 +451,8 @@ namespace ASC.Data.Storage.DiscStorage if (Directory.Exists(target)) { return Directory.GetFiles(target, "*.*", SearchOption.AllDirectories) - .Select(entry => new FileInfo(entry)) - .Sum(info => info.Length); + .Select(entry => Crypt.GetFileSize(entry)) + .Sum(); } throw new FileNotFoundException("directory not found " + target); @@ -459,9 +487,10 @@ namespace ASC.Data.Storage.DiscStorage var finfo = new FileInfo(entry); if ((DateTime.UtcNow - finfo.CreationTimeUtc) > oldThreshold) { + var size = Crypt.GetFileSize(entry); File.Delete(entry); - QuotaUsedDelete(domain, finfo.Length); + QuotaUsedDelete(domain, size); } } } @@ -548,7 +577,7 @@ namespace ASC.Data.Storage.DiscStorage if (Directory.Exists(target)) { var entries = Directory.GetFiles(target, "*.*", SearchOption.AllDirectories); - size = entries.Select(entry => new FileInfo(entry)).Select(info => info.Length).Sum(); + size = entries.Select(entry => Crypt.GetFileSize(entry)).Sum(); } return size; } @@ -569,7 +598,7 @@ namespace ASC.Data.Storage.DiscStorage File.Copy(target, newtarget, true); - var flength = new FileInfo(target).Length; + var flength = Crypt.GetFileSize(target); ; QuotaUsedAdd(newdomain, flength); } else @@ -601,8 +630,10 @@ namespace ASC.Data.Storage.DiscStorage // Copy each file into it's new directory. foreach (var fi in source.GetFiles()) { - fi.CopyTo(Path.Combine(target.ToString(), fi.Name), true); - QuotaUsedAdd(newdomain, fi.Length); + var fp = Path.Combine(target.ToString(), fi.Name); + fi.CopyTo(fp, true); + var size = Crypt.GetFileSize(fp); + QuotaUsedAdd(newdomain, size); } // Copy each subdirectory using recursion. @@ -625,11 +656,16 @@ namespace ASC.Data.Storage.DiscStorage } public Stream GetWriteStream(string domain, string path) + { + return GetWriteStream(domain, path, FileMode.Create); + } + + public Stream GetWriteStream(string domain, string path, FileMode fileMode) { if (path == null) throw new ArgumentNullException("path"); var target = GetTarget(domain, path); CreateDirectory(target); - return File.Open(target, FileMode.Create); + return File.Open(target, fileMode); } private static void CreateDirectory(string target) @@ -659,5 +695,38 @@ namespace ASC.Data.Storage.DiscStorage throw new ArgumentException("bad path"); } } + + public void Encrypt(string domain, string path) + { + if (path == null) throw new ArgumentNullException("path"); + + var target = GetTarget(domain, path); + + if (File.Exists(target)) + { + Crypt.EncryptFile(target); + } + else + { + throw new FileNotFoundException("file not found", target); + } + } + + public void Decrypt(string domain, string path) + { + if (path == null) throw new ArgumentNullException("path"); + + var target = GetTarget(domain, path); + + if (File.Exists(target)) + { + Crypt.DecryptFile(target); + } + else + { + throw new FileNotFoundException("file not found", target); + } + } } +} } \ No newline at end of file diff --git a/common/ASC.Data.Storage/Encryption/Crypt.cs b/common/ASC.Data.Storage/Encryption/Crypt.cs new file mode 100644 index 0000000000..600dd6e403 --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/Crypt.cs @@ -0,0 +1,332 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2018 + * + * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). + * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that + * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR + * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html + * + * You can contact Ascensio System SIA by email at sales@onlyoffice.com + * + * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display + * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. + * + * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains + * relevant author attributions when distributing the software. If the display of the logo in its graphic + * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" + * in every copy of the program you distribute. + * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. + * +*/ + + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace ASC.Data.Storage.Encryption +{ + public class Crypt : ICrypt + { + private readonly string storage; + private readonly EncryptionSettings settings; + private readonly string tempDir; + + public Crypt(string storageName, EncryptionSettings encryptionSettings) + { + storage = storageName; + settings = encryptionSettings; + tempDir = ConfigurationManagerExtension.AppSettings["storage.encryption.tempdir"] ?? Path.GetTempPath(); + } + + public byte Version { get { return 1; } } + + public void EncryptFile(string filePath) + { + var metadata = EncryptionFactory.GetMetadata(); + + metadata.Initialize(settings.Password); + + using (var fileStream = File.OpenRead(filePath)) + { + if (metadata.TryReadFromStream(fileStream, Version)) + { + return; + } + } + + EncryptFile(filePath, settings.Password); + } + + public void DecryptFile(string filePath) + { + if (settings.Status == EncryprtionStatus.Decrypted) + { + return; + } + + DecryptFile(filePath, settings.Password); + } + + public Stream GetReadStream(string filePath) + { + if (settings.Status == EncryprtionStatus.Decrypted) + { + return File.OpenRead(filePath); + } + + return GetReadStream(filePath, settings.Password); + } + + public long GetFileSize(string filePath) + { + if (settings.Status == EncryprtionStatus.Decrypted) + { + return new FileInfo(filePath).Length; + } + + return GetFileSize(filePath, settings.Password); + } + + + private void EncryptFile(string filePath, string password) + { + if (string.IsNullOrEmpty(password)) return; + + var fileInfo = new FileInfo(filePath); + + if (fileInfo.IsReadOnly) + { + fileInfo.IsReadOnly = false; + } + + var ecryptedFilePath = GetUniqFileName(filePath, ".enc"); + + try + { + var metadata = EncryptionFactory.GetMetadata(); + + metadata.Initialize(Version, password, fileInfo.Length); + + using (var ecryptedFileStream = new FileStream(ecryptedFilePath, FileMode.Create)) + { + metadata.WriteToStream(ecryptedFileStream); + + using (var algorithm = metadata.GetCryptographyAlgorithm()) + { + using (var transform = algorithm.CreateEncryptor()) + { + using (var cryptoStream = new CryptoStreamWrapper(ecryptedFileStream, transform, CryptoStreamMode.Write)) + { + using (var fileStream = File.OpenRead(filePath)) + { + fileStream.CopyTo(cryptoStream); + fileStream.Close(); + } + + cryptoStream.FlushFinalBlock(); + + metadata.ComputeAndWriteHmacHash(ecryptedFileStream); + + cryptoStream.Close(); + } + } + } + + ecryptedFileStream.Close(); + } + + ReplaceFile(ecryptedFilePath, filePath); + } + catch (Exception exception) + { + if (File.Exists(ecryptedFilePath)) + { + File.Delete(ecryptedFilePath); + } + + throw exception; + } + } + + private void DecryptFile(string filePath, string password) + { + var fileInfo = new FileInfo(filePath); + + if (fileInfo.IsReadOnly) + { + fileInfo.IsReadOnly = false; + } + + var decryptedFilePath = GetUniqFileName(filePath, ".dec"); + + try + { + var metadata = EncryptionFactory.GetMetadata(); + + metadata.Initialize(password); + + using (var fileStream = File.OpenRead(filePath)) + { + if (!metadata.TryReadFromStream(fileStream, Version)) return; + + metadata.ComputeAndValidateHmacHash(fileStream); + + using (var decryptedFileStream = new FileStream(decryptedFilePath, FileMode.Create)) + { + using (var algorithm = metadata.GetCryptographyAlgorithm()) + { + using (var transform = algorithm.CreateDecryptor()) + { + using (var cryptoStream = new CryptoStreamWrapper(decryptedFileStream, transform, CryptoStreamMode.Write)) + { + fileStream.CopyTo(cryptoStream); + + cryptoStream.FlushFinalBlock(); + cryptoStream.Close(); + } + } + } + + decryptedFileStream.Close(); + } + + fileStream.Close(); + } + + ReplaceFile(decryptedFilePath, filePath); + } + catch (Exception exception) + { + if (File.Exists(decryptedFilePath)) + { + File.Delete(decryptedFilePath); + } + + throw exception; + } + } + + private Stream GetReadMemoryStream(string filePath, string password) + { + var decryptedMemoryStream = new MemoryStream(); //TODO: MemoryStream or temporary decrypted file on disk? + + var metadata = EncryptionFactory.GetMetadata(); + + metadata.Initialize(password); + + var fileStream = File.OpenRead(filePath); + + if (!metadata.TryReadFromStream(fileStream, Version)) + { + decryptedMemoryStream.Close(); + fileStream.Seek(0, SeekOrigin.Begin); + return fileStream; + } + + metadata.ComputeAndValidateHmacHash(fileStream); + + using (var algorithm = metadata.GetCryptographyAlgorithm()) + { + using (var transform = algorithm.CreateDecryptor()) + { + using (var cryptoStream = new CryptoStreamWrapper(fileStream, transform, CryptoStreamMode.Read)) + { + cryptoStream.CopyTo(decryptedMemoryStream); + cryptoStream.Close(); + } + } + } + + fileStream.Close(); + + decryptedMemoryStream.Seek(0, SeekOrigin.Begin); + + return decryptedMemoryStream; + } + + private Stream GetReadStream(string filePath, string password) + { + var metadata = EncryptionFactory.GetMetadata(); + + metadata.Initialize(password); + + var fileStream = File.OpenRead(filePath); + + if (!metadata.TryReadFromStream(fileStream, Version)) + { + fileStream.Seek(0, SeekOrigin.Begin); + return fileStream; + } + + metadata.ComputeAndValidateHmacHash(fileStream); + + var wrapper = new StreamWrapper(fileStream, metadata); + + return wrapper; + } + + private long GetFileSize(string filePath, string password) + { + var metadata = EncryptionFactory.GetMetadata(); + + metadata.Initialize(password); + + using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, metadata.GetMetadataLength(), FileOptions.SequentialScan)) + { + if (metadata.TryReadFromStream(fileStream, Version)) + { + return metadata.GetFileSize(); + } + else + { + return new FileInfo(filePath).Length; + } + } + } + + + private string GetUniqFileName(string filePath, string ext) + { + var dir = string.IsNullOrEmpty(tempDir) ? Path.GetDirectoryName(filePath) : tempDir; + var name = Path.GetFileNameWithoutExtension(filePath); + var result = Path.Combine(dir, string.Format("{0}_{1}{2}", storage, name, ext)); + var index = 1; + + while (File.Exists(result)) + { + result = Path.Combine(dir, string.Format("{0}_{1}({2}){3}", storage, name, index++, ext)); + } + + return result; + } + + private void ReplaceFile(string modifiedFilePath, string originalFilePath) + { + var tempFilePath = GetUniqFileName(originalFilePath, ".tmp"); + + File.Move(originalFilePath, tempFilePath); + + try + { + File.Move(modifiedFilePath, originalFilePath); + } + catch (Exception exception) + { + File.Move(tempFilePath, originalFilePath); + throw exception; + } + finally + { + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + } + } + } +} diff --git a/common/ASC.Data.Storage/Configuration/Schema.cs b/common/ASC.Data.Storage/Encryption/CryptoStreamWrapper.cs similarity index 53% rename from common/ASC.Data.Storage/Configuration/Schema.cs rename to common/ASC.Data.Storage/Encryption/CryptoStreamWrapper.cs index 8c462abbaa..468d79253a 100644 --- a/common/ASC.Data.Storage/Configuration/Schema.cs +++ b/common/ASC.Data.Storage/Encryption/CryptoStreamWrapper.cs @@ -1,60 +1,59 @@ -/* - * - * (c) Copyright Ascensio System Limited 2010-2018 - * - * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU - * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). - * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that - * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. - * - * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR - * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html - * - * You can contact Ascensio System SIA by email at sales@onlyoffice.com - * - * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display - * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. - * - * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains - * relevant author attributions when distributing the software. If the display of the logo in its graphic - * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" - * in every copy of the program you distribute. - * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. - * -*/ - - -namespace ASC.Data.Storage.Configuration -{ - static class Schema - { - public const string SECTION_NAME = "storage"; - public const string FILE_PATH = "file"; - - public const string APPENDERS = "appender"; - public const string APPEND = "append"; - public const string APPENDSECURE = "appendssl"; - public const string EXTs = "exts"; - - public const string HANDLERS = "handler"; - public const string PROPERTIES = "properties"; - public const string PROPERTY = "property"; - - public const string MODULES = "module"; - public const string TYPE = "type"; - public const string NAME = "name"; - public const string VALUE = "value"; - public const string PATH = "path"; - public const string DATA = "data"; - public const string VIRTUALPATH = "virtualpath"; - public const string VISIBLE = "visible"; - public const string COUNT_QUOTA = "count"; - public const string APPEND_TENANT_ID = "appendTenantId"; - public const string ACL = "acl"; - public const string EXPIRES = "expires"; - public const string DOMAINS = "domain"; - public const string PUBLIC = "public"; - public const string DISABLEDMIGRATE = "disableMigrate"; - - } -} \ No newline at end of file +/* + * + * (c) Copyright Ascensio System Limited 2010-2018 + * + * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). + * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that + * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR + * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html + * + * You can contact Ascensio System SIA by email at sales@onlyoffice.com + * + * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display + * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. + * + * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains + * relevant author attributions when distributing the software. If the display of the logo in its graphic + * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" + * in every copy of the program you distribute. + * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. + * +*/ + + +using System.IO; +using System.Security.Cryptography; + +namespace ASC.Data.Storage.Encryption +{ + //https://stackoverflow.com/a/22072068 + + internal sealed class CryptoStreamWrapper : CryptoStream + { + private readonly Stream underlyingStream; + + public CryptoStreamWrapper(Stream stream, ICryptoTransform transform, CryptoStreamMode mode) + : base(stream, transform, mode) + { + underlyingStream = stream; + } + + protected override void Dispose(bool disposing) + { + try + { + base.Dispose(disposing); + } + catch (CryptographicException) + { + if (disposing) + { + underlyingStream.Dispose(); + } + } + } + } +} diff --git a/common/ASC.Data.Storage/Encryption/EncryprtionStatus.cs b/common/ASC.Data.Storage/Encryption/EncryprtionStatus.cs new file mode 100644 index 0000000000..ef5e9b857d --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/EncryprtionStatus.cs @@ -0,0 +1,36 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2018 + * + * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). + * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that + * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR + * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html + * + * You can contact Ascensio System SIA by email at sales@onlyoffice.com + * + * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display + * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. + * + * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains + * relevant author attributions when distributing the software. If the display of the logo in its graphic + * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" + * in every copy of the program you distribute. + * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. + * +*/ + + +namespace ASC.Data.Storage.Encryption +{ + public enum EncryprtionStatus + { + Decrypted, + EncryptionStarted, + Encrypted, + DecryptionStarted + } +} diff --git a/common/ASC.Data.Storage/Encryption/EncryptionFactory.cs b/common/ASC.Data.Storage/Encryption/EncryptionFactory.cs new file mode 100644 index 0000000000..0ac4fcc9c6 --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/EncryptionFactory.cs @@ -0,0 +1,41 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2018 + * + * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). + * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that + * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR + * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html + * + * You can contact Ascensio System SIA by email at sales@onlyoffice.com + * + * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display + * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. + * + * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains + * relevant author attributions when distributing the software. If the display of the logo in its graphic + * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" + * in every copy of the program you distribute. + * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. + * +*/ + + +namespace ASC.Data.Storage.Encryption +{ + class EncryptionFactory + { + public static ICrypt GetCrypt(string storageName, EncryptionSettings encryptionSettings) + { + return new Crypt(storageName, encryptionSettings); + } + + public static IMetadata GetMetadata() + { + return new Metadata(); + } + } +} diff --git a/common/ASC.Data.Storage/Encryption/EncryptionServiceClient.cs b/common/ASC.Data.Storage/Encryption/EncryptionServiceClient.cs new file mode 100644 index 0000000000..d656d4c496 --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/EncryptionServiceClient.cs @@ -0,0 +1,48 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2018 + * + * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). + * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that + * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR + * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html + * + * You can contact Ascensio System SIA by email at sales@onlyoffice.com + * + * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display + * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. + * + * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains + * relevant author attributions when distributing the software. If the display of the logo in its graphic + * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" + * in every copy of the program you distribute. + * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. + * +*/ + + +using ASC.Common.Module; + +namespace ASC.Data.Storage.Encryption +{ + public class EncryptionServiceClient : BaseWcfClient, IEncryptionService + { + public void Start(EncryptionSettings encryptionSettings, string serverRootPath) + { + Channel.Start(encryptionSettings, serverRootPath); + } + + public double GetProgress() + { + return Channel.GetProgress(); + } + + public void Stop() + { + Channel.Stop(); + } + } +} diff --git a/common/ASC.Data.Storage/Encryption/EncryptionSettings.cs b/common/ASC.Data.Storage/Encryption/EncryptionSettings.cs new file mode 100644 index 0000000000..cd7551ce67 --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/EncryptionSettings.cs @@ -0,0 +1,178 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2018 + * + * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). + * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that + * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. + * + * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR + * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html + * + * You can contact Ascensio System SIA by email at sales@onlyoffice.com + * + * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display + * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. + * + * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains + * relevant author attributions when distributing the software. If the display of the logo in its graphic + * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" + * in every copy of the program you distribute. + * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. + * +*/ + + +using System; +using System.Runtime.Serialization; +using System.Security.Cryptography; + +using ASC.Common.Caching; +using ASC.Security.Cryptography; + +namespace ASC.Data.Storage.Encryption +{ + [Serializable] + [DataContract] + public class EncryptionSettings + { + private const string key = "EncryptionSettings"; + + private string password; + + [DataMember] + public string Password + { + get { return password; } + set { password = (value ?? string.Empty).Replace('#', '_'); } + } + + [DataMember] + public EncryprtionStatus Status { get; set; } + + [DataMember] + public bool NotifyUsers { get; set; } + + public EncryptionSettings() + { + Password = string.Empty; + Status = EncryprtionStatus.Decrypted; + NotifyUsers = true; + } + + + public string Serialize() + { + return string.Join("#", + string.IsNullOrEmpty(password) ? string.Empty : InstanceCrypto.Encrypt(password), + (int)Status, + NotifyUsers + ); + } + + public static EncryptionSettings Deserialize(string value) + { + if (string.IsNullOrEmpty(value)) + { + return new EncryptionSettings(); + } + + var parts = value.Split(new[] { '#' }, StringSplitOptions.None); + + var password = string.IsNullOrEmpty(parts[0]) ? string.Empty : InstanceCrypto.Decrypt(parts[0]); + var status = int.Parse(parts[1]); + var notifyUsers = bool.Parse(parts[2]); + + return new EncryptionSettings + { + Password = password, + Status = (EncryprtionStatus)status, + NotifyUsers = notifyUsers + }; + } + + // source System.Web.Security.Membership.GeneratePassword + public static string GeneratePassword(int length, int numberOfNonAlphanumericCharacters) + { + var punctuations = "!@#$%^&*()_-+=[{]};:>|./?".ToCharArray(); + + if (length < 1 || length > 128) + { + throw new ArgumentException("password_length_incorrect", "length"); + } + + if (numberOfNonAlphanumericCharacters > length || numberOfNonAlphanumericCharacters < 0) + { + throw new ArgumentException("min_required_non_alphanumeric_characters_incorrect", "numberOfNonAlphanumericCharacters"); + } + + byte[] array = new byte[length]; + char[] array2 = new char[length]; + int num = 0; + + using (var rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(array); + } + + for (int i = 0; i < length; i++) + { + int num2 = (int)array[i] % 87; + if (num2 < 10) + { + array2[i] = (char)(48 + num2); + continue; + } + + if (num2 < 36) + { + array2[i] = (char)(65 + num2 - 10); + continue; + } + + if (num2 < 62) + { + array2[i] = (char)(97 + num2 - 36); + continue; + } + + array2[i] = punctuations[num2 - 62]; + num++; + } + + if (num < numberOfNonAlphanumericCharacters) + { + Random random = new Random(); + for (int j = 0; j < numberOfNonAlphanumericCharacters - num; j++) + { + int num3; + do + { + num3 = random.Next(0, length); + } + while (!char.IsLetterOrDigit(array2[num3])); + array2[num3] = punctuations[random.Next(0, punctuations.Length)]; + } + } + + return new string(array2); + } + + public static EncryptionSettings Load() + { + var settings = CoreContext.Configuration.GetSetting(key); + + return Deserialize(settings); + } + + public void Save() + { + var settings = Serialize(); + + CoreContext.Configuration.SaveSetting(key, settings); + + AscCache.ClearCache(); + } + } +} diff --git a/common/ASC.Data.Storage/Encryption/ICrypt.cs b/common/ASC.Data.Storage/Encryption/ICrypt.cs new file mode 100644 index 0000000000..f80adf517b --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/ICrypt.cs @@ -0,0 +1,34 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2020 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + + +using System.IO; + +namespace ASC.Data.Storage.Encryption +{ + public interface ICrypt + { + byte Version { get; } + + void EncryptFile(string filePath); + + void DecryptFile(string filePath); + + Stream GetReadStream(string filePath); + + long GetFileSize(string filePath); + } +} diff --git a/common/ASC.Data.Storage/Encryption/IEncryptionService.cs b/common/ASC.Data.Storage/Encryption/IEncryptionService.cs new file mode 100644 index 0000000000..3145be6096 --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/IEncryptionService.cs @@ -0,0 +1,34 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2020 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + + +using System.ServiceModel; + +namespace ASC.Data.Storage.Encryption +{ + [ServiceContract] + public interface IEncryptionService + { + [OperationContract] + void Start(EncryptionSettings encryptionSettings, string serverRootPath); + + [OperationContract] + double GetProgress(); + + [OperationContract] + void Stop(); + } +} diff --git a/common/ASC.Data.Storage/Encryption/IMetadata.cs b/common/ASC.Data.Storage/Encryption/IMetadata.cs new file mode 100644 index 0000000000..f3c1040cb6 --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/IMetadata.cs @@ -0,0 +1,45 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2020 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + + +using System.IO; +using System.Security.Cryptography; + +namespace ASC.Data.Storage.Encryption +{ + public interface IMetadata + { + void Initialize(string password); + + void Initialize(byte version, string password, long fileSize); + + bool TryReadFromStream(Stream stream, byte cryptVersion); + + void WriteToStream(Stream stream); + + SymmetricAlgorithm GetCryptographyAlgorithm(); + + void ComputeAndWriteHmacHash(Stream stream); + + void ComputeAndValidateHmacHash(Stream stream); + + byte GetCryptoVersion(); + + long GetFileSize(); + + int GetMetadataLength(); + } +} diff --git a/common/ASC.Data.Storage/Encryption/IntegrityProtectionException.cs b/common/ASC.Data.Storage/Encryption/IntegrityProtectionException.cs new file mode 100644 index 0000000000..9d48d8805f --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/IntegrityProtectionException.cs @@ -0,0 +1,32 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2020 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + + +using System; + +namespace ASC.Data.Storage.Encryption +{ + public class IntegrityProtectionException : Exception + { + public IntegrityProtectionException() + { + } + + public IntegrityProtectionException(string message) : base(message) + { + } + } +} diff --git a/common/ASC.Data.Storage/Encryption/Metadata.cs b/common/ASC.Data.Storage/Encryption/Metadata.cs new file mode 100644 index 0000000000..3c1ece3d52 --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/Metadata.cs @@ -0,0 +1,301 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2020 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + + +using System; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace ASC.Data.Storage.Encryption +{ + public class Metadata : IMetadata + { + private const string prefixString = "AscEncrypted"; + + private const int prefixLength = 12; // prefixString length + private const int versionLength = 1; // byte + private const int sizeLength = 8; // long (int64) + + private const int saltLength = 32; // The salt size must be 8 bytes or larger + + private const int keySize = 256; // key size, in bits, of the secret key used for the symmetric algorithm. AES-256 + private const int blockSize = 128; // block size, in bits, of the cryptographic operation. default is 128 bits + + private const int keyLength = keySize / 8; // secret key used for the symmetric algorithm. 32 bytes + private const int ivLength = blockSize / 8; // The initialization vector (IV) to use for the symmetric algorithm. 16 bytes + + private const int hmacKeyLength = 64; // HMACSHA256 64-byte private key is recommended. + private const int hmacHashLength = 32; // HMACSHA256 The output hash is 256 bits (32 bytes) in length + + private const int metadataLength = prefixLength + versionLength + sizeLength + saltLength + hmacHashLength + ivLength; + + private static int? iterations; // Rfc2898DeriveBytes: The minimum recommended number of iterations is 1000. + + private int Iterations + { + get + { + if (iterations.HasValue) + { + return iterations.Value; + } + + int iterationsCount; + + if (!int.TryParse(ConfigurationManagerExtension.AppSettings["storage.encryption.iterations"], out iterationsCount)) + { + iterationsCount = 4096; + } + + iterations = iterationsCount; + + return iterations.Value; + } + } + + private string Password; + + private byte[] Prefix; + private byte[] Version; + private byte[] Size; + + private byte[] Salt; + + private byte[] Key; + private byte[] IV; + + private byte[] HmacKey; + private byte[] HmacHash; + + + public void Initialize(string password) + { + Password = password; + + Prefix = Encoding.UTF8.GetBytes(prefixString); + Version = new byte[versionLength]; + Size = new byte[sizeLength]; + + Salt = new byte[saltLength]; + + Key = new byte[keyLength]; + IV = new byte[ivLength]; + + HmacKey = new byte[hmacKeyLength]; + HmacHash = new byte[hmacHashLength]; + } + + public void Initialize(byte version, string password, long fileSize) + { + Password = password; + + Prefix = Encoding.UTF8.GetBytes(prefixString); + Version = new byte[versionLength] { version }; + Size = LongToByteArray(fileSize); + + Salt = GenerateRandom(saltLength); + + Key = GenerateKey(); + IV = GenerateRandom(ivLength); + + HmacKey = GenerateHmacKey(); + HmacHash = new byte[hmacHashLength]; // Empty byte array. The real hmac will be computed after encryption + } + + + public bool TryReadFromStream(Stream stream, byte cryptVersion) + { + try + { + var readed = stream.Read(Prefix, 0, prefixLength); + if (readed < prefixLength) return false; + + if (Encoding.UTF8.GetString(Prefix) != prefixString) return false; + + readed = stream.Read(Version, 0, versionLength); + if (readed < versionLength) return false; + + if (Version[0] != cryptVersion) return false; + + readed = stream.Read(Size, 0, sizeLength); + if (readed < sizeLength) return false; + + if (ByteArrayToLong(Size) < 0) return false; + + readed = stream.Read(Salt, 0, saltLength); + if (readed < saltLength) return false; + + readed = stream.Read(HmacHash, 0, hmacHashLength); + if (readed < hmacHashLength) return false; + + readed = stream.Read(IV, 0, ivLength); + if (readed < ivLength) return false; + + return true; + } + catch (Exception) + { + return false; + } + } + + public void WriteToStream(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + + stream.Write(Prefix, 0, prefixLength); + stream.Write(Version, 0, versionLength); + stream.Write(Size, 0, sizeLength); + stream.Write(Salt, 0, saltLength); + stream.Write(HmacHash, 0, hmacHashLength); + stream.Write(IV, 0, ivLength); + } + + public SymmetricAlgorithm GetCryptographyAlgorithm() + { + return new RijndaelManaged + { + KeySize = keySize, + BlockSize = blockSize, + Key = Key, + IV = IV, + Padding = PaddingMode.PKCS7, + Mode = CipherMode.CBC + }; + } + + public void ComputeAndWriteHmacHash(Stream stream) + { + HmacHash = ComputeHmacHash(stream); + + stream.Seek(metadataLength - ivLength - hmacHashLength, SeekOrigin.Begin); // Move position to hmac + + stream.Write(HmacHash, 0, hmacHashLength); // Replace empty hmac with computed + } + + public void ComputeAndValidateHmacHash(Stream stream) + { + Key = GenerateKey(); + + HmacKey = GenerateHmacKey(); + + var computedHash = ComputeHmacHash(stream); + + if (!HmacHash.SequenceEqual(computedHash)) + { + stream.Close(); + + throw new IntegrityProtectionException("Invalid signature"); + } + + stream.Seek(metadataLength, SeekOrigin.Begin); // Move position to encrypted data + } + + public byte GetCryptoVersion() + { + return Version[0]; + } + + public long GetFileSize() + { + return ByteArrayToLong(Size); + } + + public int GetMetadataLength() + { + return metadataLength; + } + + + private byte[] GenerateRandom(int length) + { + var random = new byte[length]; + + using (var rng = new RNGCryptoServiceProvider()) + { + rng.GetBytes(random); + } + + return random; + } + + private byte[] GenerateKey() + { + var key = new byte[keyLength]; + + using (var deriveBytes = new Rfc2898DeriveBytes(Password, Salt, Iterations, HashAlgorithmName.SHA256)) + { + key = deriveBytes.GetBytes(keyLength); + } + + return key; + } + + private byte[] GenerateHmacKey() + { + var hmacKey = new byte[hmacKeyLength]; + + using (var sha512 = new SHA512Managed()) + { + hmacKey = sha512.ComputeHash(Key); + } + + return hmacKey; + } + + private byte[] ComputeHmacHash(Stream stream) + { + var hmacHash = new byte[hmacHashLength]; + + stream.Seek(metadataLength - ivLength, SeekOrigin.Begin); // Move position to (IV + encrypted data) + + using (var hmac = new HMACSHA256(HmacKey)) + { + hmacHash = hmac.ComputeHash(stream); // IV needs to be part of the MAC calculation + } + + return hmacHash; + } + + private byte[] LongToByteArray(long value) + { + var result = BitConverter.GetBytes(value); + + if (!BitConverter.IsLittleEndian) + Array.Reverse(result); + + return result; + } + + private long ByteArrayToLong(byte[] value) + { + if (!BitConverter.IsLittleEndian) + Array.Reverse(value); + + try + { + return BitConverter.ToInt64(value, 0); + } + catch (Exception) + { + return -1; + } + } + } +} diff --git a/common/ASC.Data.Storage/Encryption/StreamWrapper.cs b/common/ASC.Data.Storage/Encryption/StreamWrapper.cs new file mode 100644 index 0000000000..b542c2efd6 --- /dev/null +++ b/common/ASC.Data.Storage/Encryption/StreamWrapper.cs @@ -0,0 +1,110 @@ +/* + * + * (c) Copyright Ascensio System Limited 2010-2020 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace ASC.Data.Storage.Encryption +{ + internal sealed class StreamWrapper : Stream + { + private readonly Stream stream; + private readonly CryptoStream cryptoStream; + private readonly SymmetricAlgorithm symmetricAlgorithm; + private readonly ICryptoTransform cryptoTransform; + private readonly long fileSize; + private readonly long metadataLength; + + public StreamWrapper(Stream fileStream, IMetadata metadata) + { + stream = fileStream; + symmetricAlgorithm = metadata.GetCryptographyAlgorithm(); + cryptoTransform = symmetricAlgorithm.CreateDecryptor(); + cryptoStream = new CryptoStreamWrapper(stream, cryptoTransform, CryptoStreamMode.Read); + fileSize = metadata.GetFileSize(); + metadataLength = metadata.GetMetadataLength(); + } + + public override bool CanRead + { + get { return stream.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return fileSize; } + } + + public override long Position + { + get + { + return stream.Position - metadataLength; + } + set + { + if (value < 0 || value > fileSize) + throw new ArgumentOutOfRangeException(); + + stream.Position = value + metadataLength; + } + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return cryptoStream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override void Close() + { + cryptoStream.Dispose(); + stream.Dispose(); + symmetricAlgorithm.Dispose(); + cryptoTransform.Dispose(); + } + } +} diff --git a/common/ASC.Data.Storage/S3/S3Storage.cs b/common/ASC.Data.Storage/S3/S3Storage.cs index 280ae19af9..453130b699 100644 --- a/common/ASC.Data.Storage/S3/S3Storage.cs +++ b/common/ASC.Data.Storage/S3/S3Storage.cs @@ -1016,27 +1016,33 @@ namespace ASC.Data.Storage.S3 _recycleDir = props["recycleDir"]; } - if (props.ContainsKey("region")) + if (props.ContainsKey("region") && !string.IsNullOrEmpty(props["region"])) { _region = props["region"]; } - if (props.ContainsKey("serviceurl")) + if (props.ContainsKey("serviceurl") && !string.IsNullOrEmpty(props["serviceurl"])) { _serviceurl = props["serviceurl"]; } if (props.ContainsKey("forcepathstyle")) { - _forcepathstyle = bool.Parse(props["forcepathstyle"]); + if (bool.TryParse(props["forcepathstyle"], out var fps)) + { + _forcepathstyle = fps; + } } if (props.ContainsKey("usehttp")) { - _useHttp = bool.Parse(props["usehttp"]); + if (bool.TryParse(props["usehttp"], out var uh)) + { + _useHttp = uh; + } } - if (props.ContainsKey("sse")) + if (props.ContainsKey("sse") && !string.IsNullOrEmpty(props["sse"])) { switch (props["sse"].ToLower()) { diff --git a/common/services/ASC.Data.Backup/Service/BackupWorker.cs b/common/services/ASC.Data.Backup/Service/BackupWorker.cs index 2449986e0a..53ed5c5e3b 100644 --- a/common/services/ASC.Data.Backup/Service/BackupWorker.cs +++ b/common/services/ASC.Data.Backup/Service/BackupWorker.cs @@ -370,10 +370,12 @@ namespace ASC.Data.Backup.Service using var scope = ServiceProvider.CreateScope(); var scopeClass = scope.ServiceProvider.GetService(); - var (tenantManager, backupStorageFactory, notifyHelper, backupRepository, backupWorker, backupPortalTask, _, _) = scopeClass; + var (tenantManager, backupStorageFactory, notifyHelper, backupRepository, backupWorker, backupPortalTask, _, _, coreBaseSettings) = scopeClass; var tenant = tenantManager.GetTenant(TenantId); - var backupName = string.Format("{0}_{1:yyyy-MM-dd_HH-mm-ss}.{2}", tenant.TenantAlias, DateTime.UtcNow, ArchiveFormat); + var dateTime = coreBaseSettings.Standalone ? DateTime.Now : DateTime.UtcNow; + var backupName = string.Format("{0}_{1:yyyy-MM-dd_HH-mm-ss}.{2}", tenantManager.GetTenant(TenantId).TenantAlias, dateTime, ArchiveFormat); + var tempFile = Path.Combine(TempFolder, backupName); var storagePath = tempFile; try @@ -502,7 +504,7 @@ namespace ASC.Data.Backup.Service { using var scope = ServiceProvider.CreateScope(); var scopeClass = scope.ServiceProvider.GetService(); - var (tenantManager, backupStorageFactory, notifyHelper, _, backupWorker, _, restorePortalTask, _) = scopeClass; + var (tenantManager, backupStorageFactory, notifyHelper, _, backupWorker, _, restorePortalTask, _, _) = scopeClass; Tenant tenant = null; var tempFile = PathHelper.GetTempFileName(TempFolder); try @@ -661,7 +663,7 @@ namespace ASC.Data.Backup.Service { using var scope = ServiceProvider.CreateScope(); var scopeClass = scope.ServiceProvider.GetService(); - var (tenantManager, _, notifyHelper, _, backupWorker, _, _, transferPortalTask) = scopeClass; + var (tenantManager, _, notifyHelper, _, backupWorker, _, _, transferPortalTask, _) = scopeClass; var tempFile = PathHelper.GetTempFileName(TempFolder); var tenant = tenantManager.GetTenant(TenantId); var alias = tenant.TenantAlias; @@ -800,6 +802,7 @@ namespace ASC.Data.Backup.Service private BackupPortalTask BackupPortalTask { get; } private RestorePortalTask RestorePortalTask { get; } private TransferPortalTask TransferPortalTask { get; } + public CoreBaseSettings CoreBaseSettings { get; } public BackupWorkerScope(TenantManager tenantManager, BackupStorageFactory backupStorageFactory, @@ -808,7 +811,8 @@ namespace ASC.Data.Backup.Service BackupWorker backupWorker, BackupPortalTask backupPortalTask, RestorePortalTask restorePortalTask, - TransferPortalTask transferPortalTask) + TransferPortalTask transferPortalTask, + CoreBaseSettings coreBaseSettings) { TenantManager = tenantManager; BackupStorageFactory = backupStorageFactory; @@ -818,6 +822,7 @@ namespace ASC.Data.Backup.Service BackupPortalTask = backupPortalTask; RestorePortalTask = restorePortalTask; TransferPortalTask = transferPortalTask; + CoreBaseSettings = coreBaseSettings; } public void Deconstruct(out TenantManager tenantManager, @@ -827,7 +832,8 @@ namespace ASC.Data.Backup.Service out BackupWorker backupWorker, out BackupPortalTask backupPortalTask, out RestorePortalTask restorePortalTask, - out TransferPortalTask transferPortalTask) + out TransferPortalTask transferPortalTask, + out CoreBaseSettings coreBaseSettings) { tenantManager = TenantManager; backupStorageFactory = BackupStorageFactory; @@ -837,6 +843,7 @@ namespace ASC.Data.Backup.Service backupPortalTask = BackupPortalTask; restorePortalTask = RestorePortalTask; transferPortalTask = TransferPortalTask; + coreBaseSettings = CoreBaseSettings; } } diff --git a/common/services/ASC.Data.Backup/Tasks/Modules/CrmModuleSpecifics.cs b/common/services/ASC.Data.Backup/Tasks/Modules/CrmModuleSpecifics.cs index d741464534..a9302f3f3f 100644 --- a/common/services/ASC.Data.Backup/Tasks/Modules/CrmModuleSpecifics.cs +++ b/common/services/ASC.Data.Backup/Tasks/Modules/CrmModuleSpecifics.cs @@ -261,7 +261,7 @@ namespace ASC.Data.Backup.Tasks.Modules { value = Regex.Replace( Convert.ToString(value), - @"(?<=""message_id"":|/products/crm/httphandlers/filehandler\.ashx\?action=mailmessage&message_id=)\d+", + @"(?<=""message_id"":|/Products/CRM/HttpHandlers/filehandler\.ashx\?action=mailmessage&message_id=)\d+", match => { var mappedMessageId = Convert.ToString(columnMapper.GetMapping(relation.ParentTable, relation.ParentColumn, match.Value)); diff --git a/common/services/ASC.Data.Backup/Tasks/PortalTaskBase.cs b/common/services/ASC.Data.Backup/Tasks/PortalTaskBase.cs index df9c325652..5f56247f42 100644 --- a/common/services/ASC.Data.Backup/Tasks/PortalTaskBase.cs +++ b/common/services/ASC.Data.Backup/Tasks/PortalTaskBase.cs @@ -129,7 +129,7 @@ namespace ASC.Data.Backup.Tasks return files.Distinct(); } - protected virtual bool IsStorageModuleAllowed(string storageModuleName) + protected bool IsStorageModuleAllowed(string storageModuleName) { var allowedStorageModules = new List { @@ -144,7 +144,9 @@ namespace ASC.Data.Backup.Tasks "fckuploaders", "talk", "mailaggregator", - "whitelabel" + "whitelabel", + "customnavigation", + "userPhotos" }; if (!allowedStorageModules.Contains(storageModuleName)) diff --git a/common/services/ASC.Data.Backup/Tasks/RestorePortalTask.cs b/common/services/ASC.Data.Backup/Tasks/RestorePortalTask.cs index 9806555970..fdecfdce67 100644 --- a/common/services/ASC.Data.Backup/Tasks/RestorePortalTask.cs +++ b/common/services/ASC.Data.Backup/Tasks/RestorePortalTask.cs @@ -55,15 +55,26 @@ namespace ASC.Data.Backup.Tasks public bool ReplaceDate { get; set; } public bool Dump { get; set; } private CoreBaseSettings CoreBaseSettings { get; set; } - private LicenseReader LicenseReader { get; set; } + private LicenseReader LicenseReader { get; set; } + public TenantManager TenantManager { get; } private AscCacheNotify AscCacheNotify { get; set; } private IOptionsMonitor Options { get; set; } - public RestorePortalTask(DbFactory dbFactory, IOptionsMonitor options, StorageFactory storageFactory, StorageFactoryConfig storageFactoryConfig, CoreBaseSettings coreBaseSettings, LicenseReader licenseReader, AscCacheNotify ascCacheNotify, ModuleProvider moduleProvider) + public RestorePortalTask( + DbFactory dbFactory, + IOptionsMonitor options, + StorageFactory storageFactory, + StorageFactoryConfig storageFactoryConfig, + CoreBaseSettings coreBaseSettings, + LicenseReader licenseReader, + TenantManager tenantManager, + AscCacheNotify ascCacheNotify, + ModuleProvider moduleProvider) : base(dbFactory, options, storageFactory, storageFactoryConfig, moduleProvider) { CoreBaseSettings = coreBaseSettings; - LicenseReader = licenseReader; + LicenseReader = licenseReader; + TenantManager = tenantManager; AscCacheNotify = ascCacheNotify; Options = options; } @@ -119,7 +130,13 @@ namespace ASC.Data.Backup.Tasks Logger.Debug("end restore data"); if (ProcessStorage) - { + { + if (CoreBaseSettings.Standalone) + { + Logger.Debug("clear cache"); + AscCacheNotify.ClearCache(); + } + DoRestoreStorage(dataReader); } if (UnblockPortalAfterCompleted) @@ -160,7 +177,23 @@ namespace ASC.Data.Backup.Tasks var stepscount = keys.Count * 2 + upgrades.Count; - SetStepsCount(ProcessStorage ? stepscount + 1 : stepscount); + SetStepsCount(ProcessStorage ? stepscount + 1 : stepscount); + + if (ProcessStorage) + { + var storageModules = StorageFactoryConfig.GetModuleList(ConfigPath).Where(IsStorageModuleAllowed); + var tenants = TenantManager.GetTenants(false); + + stepscount += storageModules.Count() * tenants.Count; + + SetStepsCount(stepscount + 1); + + DoDeleteStorage(storageModules, tenants); + } + else + { + SetStepsCount(stepscount); + } for (var i = 0; i < keys.Count; i += TasksLimit) { @@ -284,6 +317,42 @@ namespace ASC.Data.Backup.Tasks SetStepCompleted(); } Logger.Debug("end restore storage"); + } + + private void DoDeleteStorage(IEnumerable storageModules, IEnumerable tenants) + { + Logger.Debug("begin delete storage"); + + foreach (var tenant in tenants) + { + foreach (var module in storageModules) + { + var storage = StorageFactory.GetStorage(ConfigPath, tenant.TenantId.ToString(), module); + var domains = StorageFactoryConfig.GetDomainList(ConfigPath, module).ToList(); + + domains.Add(string.Empty); //instead storage.DeleteFiles("\\", "*.*", true); + + foreach (var domain in domains) + { + ActionInvoker.Try( + state => + { + if (storage.IsDirectory((string)state)) + { + storage.DeleteFiles((string)state, "\\", "*.*", true); + } + }, + domain, + 5, + onFailure: error => Logger.WarnFormat("Can't delete files for domain {0}: \r\n{1}", domain, error) + ); + } + + SetStepCompleted(); + } + } + + Logger.Debug("end delete storage"); } private IEnumerable GetFilesToProcess(IDataReadOperator dataReader) @@ -299,14 +368,6 @@ namespace ASC.Data.Backup.Tasks } } - - protected override bool IsStorageModuleAllowed(string storageModuleName) - { - if (storageModuleName == "fckuploaders") - return false; - return base.IsStorageModuleAllowed(storageModuleName); - } - private void SetTenantActive(int tenantId) { using (var connection = DbFactory.OpenConnection())