// Copyright (C) 2015, The Duplicati Team // http://www.duplicati.com, info@duplicati.com // // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation; either version 2.1 of the // License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; using System.Text; using Duplicati.Library.Main.Database; namespace Duplicati.Library.Main { public class Utility { /// /// Implementation of the IMetahash interface /// private class Metahash : IMetahash { /// /// The base64 encoded hash /// private readonly string m_filehash; /// /// The UTF-8 encoded json element with the metadata /// private readonly byte[] m_blob; /// /// The lookup table with elements /// private readonly Dictionary m_values; public Metahash(Dictionary values, Options options) { m_values = values; using (var ms = new System.IO.MemoryStream()) using (var w = new StreamWriter(ms, Encoding.UTF8)) using(var filehasher = Library.Utility.HashAlgorithmHelper.Create(options.FileHashAlgorithm)) { if (filehasher == null) throw new Duplicati.Library.Interface.UserInformationException(Strings.Common.InvalidHashAlgorithm(options.FileHashAlgorithm), "FileHashAlgorithmNotSupported"); w.Write(JsonConvert.SerializeObject(values)); w.Flush(); m_blob = ms.ToArray(); ms.Position = 0; m_filehash = Convert.ToBase64String(filehasher.ComputeHash(ms)); } } public string FileHash { get { return m_filehash; } } public long Size { get { return m_blob.Length; } } public byte[] Blob { get { return m_blob; } } public Dictionary Values { get { return m_values; } } } /// /// Constructs a container for a given metadata dictionary /// /// The metadata values to wrap /// A IMetahash instance public static IMetahash WrapMetadata(Dictionary values, Options options) { return new Metahash(values, options); } internal static void UpdateOptionsFromDb(LocalDatabase db, Options options, System.Data.IDbTransaction transaction = null) { string n = null; var opts = db.GetDbOptions(transaction); if(opts.ContainsKey("blocksize") && (!options.RawOptions.TryGetValue("blocksize", out n) || string.IsNullOrEmpty(n))) options.RawOptions["blocksize"] = opts["blocksize"] + "b"; if (opts.ContainsKey("blockhash") && (!options.RawOptions.TryGetValue("block-hash-algorithm", out n) || string.IsNullOrEmpty(n))) options.RawOptions["block-hash-algorithm"] = opts["blockhash"]; if (opts.ContainsKey("filehash") && (!options.RawOptions.TryGetValue("file-hash-algorithm", out n) || string.IsNullOrEmpty(n))) options.RawOptions["file-hash-algorithm"] = opts["filehash"]; } internal static void VerifyParameters(LocalDatabase db, Options options, System.Data.IDbTransaction transaction = null) { var newDict = new Dictionary(); newDict.Add("blocksize", options.Blocksize.ToString()); newDict.Add("blockhash", options.BlockHashAlgorithm); newDict.Add("filehash", options.FileHashAlgorithm); var opts = db.GetDbOptions(transaction); if (options.NoEncryption) { newDict.Add("passphrase", "no-encryption"); } else { string salt; opts.TryGetValue("passphrase-salt", out salt); if (string.IsNullOrEmpty(salt)) { // Not Crypto-class PRNG salts var buf = new byte[32]; new Random().NextBytes(buf); //Add version so we can detect and change the algorithm salt = "v1:" + Library.Utility.Utility.ByteArrayAsHexString(buf); } newDict["passphrase-salt"] = salt; // We avoid storing the passphrase directly, // instead we salt and rehash repeatedly newDict.Add("passphrase", Library.Utility.Utility.ByteArrayAsHexString(Library.Utility.Utility.RepeatedHashWithSalt(options.Passphrase, salt, 1200))); } var needsUpdate = false; foreach(var k in newDict) if (!opts.ContainsKey(k.Key)) needsUpdate = true; else if (opts[k.Key] != k.Value) { if (k.Key == "passphrase") { if (!options.AllowPassphraseChange) { if (newDict[k.Key] == "no-encryption") throw new Duplicati.Library.Interface.UserInformationException("You have attempted to remove the passphrase on an existing backup, which is not supported. Please configure a new clean backup if you want to remove the passphrase.", "PassphraseRemovalNotSupported"); else if (opts[k.Key] == "no-encryption") throw new Duplicati.Library.Interface.UserInformationException("You have attempted to add a passphrase to an existing backup, which is not supported. Please configure a new clean backup if you want to add a passphrase.", "PassphraseAdditionNotSupported"); else throw new Duplicati.Library.Interface.UserInformationException("You have attempted to change a passphrase to an existing backup, which is not supported. Please configure a new clean backup if you want to change the passphrase.", "PassphraseChangeNotSupported"); } } else throw new Duplicati.Library.Interface.UserInformationException(string.Format("You have attempted to change the parameter \"{0}\" from \"{1}\" to \"{2}\", which is not supported. Please configure a new clean backup if you want to change the parameter.", k.Key, opts[k.Key], k.Value), "ParameterChangeNotSupported"); } //Extra sanity check if (db.GetBlocksLargerThan(options.Blocksize) > 0) throw new Duplicati.Library.Interface.UserInformationException("You have attempted to change the block-size on an existing backup, which is not supported. Please configure a new clean backup if you want to change the block-size.", "BlockSizeChangeNotSupported"); if (needsUpdate) { // Make sure we do not lose values foreach(var k in opts) if (!newDict.ContainsKey(k.Key)) newDict[k.Key] = k.Value; db.SetDbOptions(newDict, transaction); } } /// /// The filename for the marker file that the user can add to suppress donation messages /// private const string SUPPRESS_DONATIONS_FILENAME = "suppress_donation_messages.txt"; /// /// Gets or sets donation message suppression /// public static bool SuppressDonationMessages { get { try { var folder = Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AutoUpdater.AutoUpdateSettings.AppName); return File.Exists(Path.Combine(folder, SUPPRESS_DONATIONS_FILENAME)); } catch { } return true; } set { try { var folder = Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AutoUpdater.AutoUpdateSettings.AppName); if (!Directory.Exists(folder)) Directory.CreateDirectory(folder); var path = Path.Combine(folder, SUPPRESS_DONATIONS_FILENAME); if (value) using(File.OpenWrite(path)) { } else File.Delete(path); } catch { } } } } }