示例#1
0
        /// <summary>
        /// Replaces an entry within its parent's collection.
        /// </summary>
        /// <param name="document">The document being updated.</param>
        /// <param name="parent">The parent to update.</param>
        /// <param name="child">The entry to use as a replacement.</param>
        /// <param name="touchesNode">Whether to treat the swap as an "update" (vs a revert).</param>
        protected override void SwapIntoParent(KdbxDocument document, IKeePassGroup parent, IKeePassEntry child, bool touchesNode)
        {
            if (document == null)
            {
                throw new ArgumentNullException(nameof(document));
            }

            if (parent == null)
            {
                throw new ArgumentNullException(nameof(parent));
            }

            if (child == null)
            {
                throw new ArgumentNullException(nameof(child));
            }

            // Otherwise, we need to find the equivalent existing child (by UUID) and
            // update that way.
            IKeePassNode  matchedNode  = parent.Children.First(g => g.Uuid.Equals(child.Uuid));
            IKeePassEntry matchedEntry = matchedNode as IKeePassEntry;

            DebugHelper.Assert(matchedEntry != null);
            matchedEntry.SyncTo(child, touchesNode);
        }
示例#2
0
        public async Task DowngradeCipherSettings()
        {
            DateTime lastPasswordChange = this.document.Metadata.MasterKeyChanged.Value;

            Assert.AreEqual(EncryptionAlgorithm.ChaCha20, this.settingsVm.Cipher, "ChaCha20 should be the encryption algorithm before the test starts");
            this.writer.Cipher = EncryptionAlgorithm.Aes;

            Assert.IsInstanceOfType(this.settingsVm.GetKdfParameters(), typeof(Argon2Parameters), "Argon2 should be the KDF before the test starts according to the VM");
            Assert.IsInstanceOfType(this.writer.KdfParameters, typeof(Argon2Parameters), "Argon2 should be the KDF before the test starts according to the KdbxWriter");

            this.settingsVm.KdfGuid       = AesParameters.AesUuid;
            this.settingsVm.KdfIterations = 6001;

            Assert.IsInstanceOfType(this.writer.KdfParameters, typeof(AesParameters), "Changes to the settings VM should be reflected in the KdbxWriter");
            Assert.IsTrue(await this.persistenceService.Save(this.document));

            KdbxReader reader = new KdbxReader();

            using (IRandomAccessStream stream = await this.saveFile.AsIStorageFile.OpenReadAsync())
            {
                await reader.ReadHeaderAsync(stream, CancellationToken.None);

                Assert.AreEqual(EncryptionAlgorithm.Aes, reader.HeaderData.Cipher, "New reader should have the correct cipher");
                AesParameters aesParams = reader.HeaderData.KdfParameters as AesParameters;
                Assert.IsNotNull(aesParams, "Database should have properly persisted with AES");
                Assert.AreEqual(6001, (int)aesParams.Rounds, "AES iteration count should have been persisted correctly");

                KdbxDecryptionResult decryption = await reader.DecryptFileAsync(stream, this.dbPassword, this.dbKeyFile, CancellationToken.None);

                Assert.AreEqual(KdbxParserCode.Success, decryption.Result.Code);
                KdbxDocument document = decryption.GetDocument();
                Assert.AreEqual(lastPasswordChange, document.Metadata.MasterKeyChanged.Value, "MasterKeyChanged timestamp should not have changed");
            }
        }
示例#3
0
        /// <summary>
        /// Creates a ViewModel wrapping a brand new KdbxGroup as a child of the specified parent group.
        /// </summary>
        /// <param name="resourceProvider">IResourceProvider for localizing strings.</param>
        /// <param name="navigationViewModel">A ViewModel used for tracking navigation history.</param>
        /// <param name="persistenceService">A service used for persisting the document.</param>
        /// <param name="clipboardService">A service used for accessing the clipboard.</param>
        /// <param name="settingsService">A service used for accessing app settings.</param>
        /// <param name="document">A KdbxDocument representing the database we are working on.</param>
        /// <param name="parentGroup">The IKeePassGroup to use as a parent for the new group.</param>
        /// <param name="rng">A random number generator used to protect strings in memory.</param>
        public EntryDetailsViewModel(
            IResourceProvider resourceProvider,
            IDatabaseNavigationViewModel navigationViewModel,
            IDatabasePersistenceService persistenceService,
            ISensitiveClipboardService clipboardService,
            IAppSettingsService settingsService,
            KdbxDocument document,
            IKeePassGroup parentGroup,
            IRandomNumberGenerator rng
            ) : this(
                resourceProvider,
                navigationViewModel,
                persistenceService,
                clipboardService,
                settingsService,
                document,
                new KdbxEntry(parentGroup, rng, document.Metadata),
                true,
                false,
                rng
                )
        {
            if (parentGroup == null)
            {
                throw new ArgumentNullException(nameof(parentGroup));
            }

            if (rng == null)
            {
                throw new ArgumentNullException(nameof(rng));
            }
        }
示例#4
0
        public async Task Initialize()
        {
            CancellationTokenSource cts = new CancellationTokenSource();

            MethodInfo testMethod = GetType().GetRuntimeMethod(
                TestContext.TestName, new Type[0]
                );

            var specAttr = testMethod.GetCustomAttribute <DetailsForAttribute>();
            var dataAttr = testMethod.GetCustomAttribute <TestDataAttribute>();

            Assert.IsTrue(specAttr != null || dataAttr != null);

            try
            {
                Utils.DatabaseInfo databaseInfo = await Utils.GetDatabaseInfoForTest(TestContext);

                KdbxReader reader = new KdbxReader();

                using (IRandomAccessStream stream = await databaseInfo.Database.AsIStorageFile.OpenReadAsync())
                {
                    Assert.IsFalse((await reader.ReadHeaderAsync(stream, cts.Token)).IsError);
                    KdbxDecryptionResult decryption = await reader.DecryptFileAsync(stream, databaseInfo.Password, databaseInfo.Keyfile, cts.Token);

                    Assert.IsFalse(decryption.Result.IsError);
                    this.document = decryption.GetDocument();

                    if (specAttr != null && (dataAttr == null || !dataAttr.SkipInitialization))
                    {
                        IDatabaseNavigationViewModel navVm = new DatabaseNavigationViewModel();
                        navVm.SetGroup(this.document.Root.DatabaseGroup);

                        IDatabasePersistenceService persistenceService = new DummyPersistenceService();

                        this.instantiationTime = DateTime.Now;
                        if (specAttr.IsNew)
                        {
                            this.expectedParent = this.document.Root.DatabaseGroup;
                            this.viewModel      = GetNewViewModel(navVm, persistenceService, this.document, this.expectedParent);
                        }
                        else
                        {
                            this.expectedParent = this.document.Root.DatabaseGroup;
                            this.viewModel      = GetExistingViewModel(
                                navVm,
                                persistenceService,
                                this.document,
                                specAttr.IsOpenedReadOnly
                                );
                        }
                    }
                    else
                    {
                        this.expectedParent = null;
                        Assert.IsTrue(dataAttr.SkipInitialization);
                    }
                }
            }
            catch (InvalidOperationException) { }
        }
示例#5
0
        private async Task RoundTrip()
        {
            ReaderResult initialHeaderResult = await this.reader.ReadHeaderAsync(await this.thisTestInfo.Database.AsIStorageFile.OpenReadAsync(), CancellationToken.None);

            Assert.AreEqual(ReaderResult.Success, initialHeaderResult, "Initial header read should be successful");

            KdbxDecryptionResult result = await this.reader.DecryptFileAsync(await this.thisTestInfo.Database.AsIStorageFile.OpenReadAsync(), this.thisTestInfo.Password, this.thisTestInfo.Keyfile, CancellationToken.None);

            Assert.AreEqual(ReaderResult.Success, result.Result, "File should have initially decrypted properly");
            KdbxDocument kdbxDoc = result.GetDocument();
            IKdbxWriter  writer  = this.reader.GetWriter();

            using (var stream = new InMemoryRandomAccessStream())
            {
                bool writeResult = await writer.WriteAsync(stream, kdbxDoc, CancellationToken.None);

                Assert.IsTrue(writeResult, "File should have written successfully");

                stream.Seek(0);
                KdbxReader   newReader = new KdbxReader();
                ReaderResult result2   = await newReader.ReadHeaderAsync(stream, CancellationToken.None);

                Assert.AreEqual(ReaderResult.Success, result2, "Header should been read back successfully after write");

                KdbxDecryptionResult result3 = await newReader.DecryptFileAsync(stream, this.thisTestInfo.Password, this.thisTestInfo.Keyfile, CancellationToken.None);

                Assert.AreEqual(ReaderResult.Success, result3.Result, "File should have decrypted successfully after write");

                KdbxDocument roundTrippedDocument = result3.GetDocument();
                Assert.AreEqual(kdbxDoc, roundTrippedDocument, "Round-tripped document should be equal to original document");
            }
        }
示例#6
0
        /// <summary>
        /// Replaces a group within its parent's collection.
        /// </summary>
        /// <param name="document">The document being updated.</param>
        /// <param name="parent">The parent to update.</param>
        /// <param name="child">The group to use as a replacement.</param>
        /// <param name="touchesNode">Whether to treat the swap as an "update" (vs a revert).</param>
        protected override void SwapIntoParent(KdbxDocument document, IKeePassGroup parent, IKeePassGroup child, bool touchesNode)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }

            if (child == null)
            {
                throw new ArgumentNullException("child");
            }

            if (child.Parent != parent)
            {
                throw new ArgumentException("child.Parent != parent");
            }

            if (parent == null)
            {
                // If there is no parent, we are updating the root database group.
                // So, just update it.
                document.Root.DatabaseGroup.SyncTo(child, touchesNode);
            }
            else
            {
                // Otherwise, we need to find the equivalent existing child (by UUID) and
                // update that way.
                IKeePassNode  matchedNode  = parent.Children.First(node => node.Uuid.Equals(child.Uuid));
                IKeePassGroup matchedGroup = matchedNode as IKeePassGroup;
                DebugHelper.Assert(matchedGroup != null);
                matchedGroup.SyncTo(child, touchesNode);
            }
        }
示例#7
0
        public async Task ChangePassword()
        {
            string newPw = "TestPW";

            this.masterKeyVm.MasterPassword = newPw;
            Assert.IsFalse(this.masterKeyVm.ConfirmCommand.CanExecute(null), "Should not be able to confirm new password until password is entered twice");
            this.masterKeyVm.ConfirmedPassword = newPw;
            Assert.IsTrue(this.masterKeyVm.ConfirmCommand.CanExecute(null), "Should be able to confirm new password when second password matches");
            this.masterKeyVm.ConfirmedPassword = "******";
            Assert.IsFalse(this.masterKeyVm.ConfirmCommand.CanExecute(null), "Mismatched passwords should not be able to be confirmed");
            this.masterKeyVm.ConfirmedPassword = newPw;

            DateTime lastPasswordChange = this.document.Metadata.MasterKeyChanged.Value;

            this.masterKeyVm.ConfirmCommand.Execute(null);
            DateTime passwordChangeTime = DateTime.Parse(this.document.Metadata.MasterKeyChanged.Value.ToString());

            Assert.IsTrue(passwordChangeTime > lastPasswordChange, "MasterKeyChanged value should have changed in document metadata");

            Assert.IsTrue(await this.persistenceService.Save(this.document));

            KdbxReader reader = new KdbxReader();

            using (IRandomAccessStream stream = await this.saveFile.AsIStorageFile.OpenReadAsync())
            {
                await reader.ReadHeaderAsync(stream, CancellationToken.None);

                KdbxDecryptionResult decryption = await reader.DecryptFileAsync(stream, newPw, null, CancellationToken.None);

                Assert.AreEqual(KdbxParserCode.Success, decryption.Result.Code, "Database should decrypt with the new credentials");
                KdbxDocument document = decryption.GetDocument();
                Assert.AreEqual(passwordChangeTime, document.Metadata.MasterKeyChanged.Value, "MasterKeyChanged timestamp should have been persisted");
            }
        }
示例#8
0
        public async Task Init()
        {
            // Get database from test attributes
            Utils.DatabaseInfo dbInfo = await Utils.GetDatabaseInfoForTest(TestContext);

            this.dbPassword = dbInfo.Password;
            this.dbKeyFile  = dbInfo.Keyfile;

            // Assert that databases named *ReadOnly* are actually readonly after a clone
            if (dbInfo.Database.Name.IndexOf("ReadOnly", StringComparison.OrdinalIgnoreCase) >= 0)
            {
                Assert.IsFalse(
                    await dbInfo.Database.CheckWritableAsync(),
                    $"This file is expected to be read-only; please verify this before testing: {dbInfo.Database.Name}"
                    );
            }

            this.saveFile = (await dbInfo.Database.AsIStorageFile.CopyAsync(
                                 ApplicationData.Current.TemporaryFolder,
                                 $"PersistenceTestDb-{Guid.NewGuid()}.kdbx",
                                 NameCollisionOption.ReplaceExisting
                                 )).AsWrapper();

            // Use a KdbxReader to parse the database and get a corresponding writer
            KdbxReader reader = new KdbxReader();

            using (IRandomAccessStream stream = await this.saveFile.AsIStorageFile.OpenReadAsync())
            {
                await reader.ReadHeaderAsync(stream, CancellationToken.None);

                KdbxDecryptionResult decryption = await reader.DecryptFileAsync(stream, dbInfo.Password, dbInfo.Keyfile, CancellationToken.None);

                Assert.AreEqual(KdbxParserCode.Success, decryption.Result.Code);
                this.document = decryption.GetDocument();
            }

            // Construct services we can use for the test
            this.writer             = reader.GetWriter();
            this.persistenceService = new DefaultFilePersistenceService(
                this.writer,
                this.writer,
                new StorageFileDatabaseCandidate(this.saveFile, true),
                new MockSyncContext(),
                true
                );
            this.credentialStorage = new MockCredentialProvider();
            this.masterKeyVm       = new MasterKeyChangeViewModel(
                this.document,
                this.saveFile,
                new DatabaseCredentialProvider(this.persistenceService, this.credentialStorage),
                new MockFileService()
                );
            this.settingsVm = new DatabaseSettingsViewModel(this.writer);
        }
示例#9
0
 /// <summary>
 /// Initializes dependencies.
 /// </summary>
 /// <param name="document">The document whose master key may be updated.</param>
 /// <param name="databaseFile">The underlying file to persist changes to.</param>
 /// <param name="credentialProvider">A provider that manages changes to credentials for the database.</param>
 /// <param name="fileService">The service used to pick a new keyfile.</param>
 public MasterKeyChangeViewModel(
     KdbxDocument document,
     ITestableFile databaseFile,
     IDatabaseCredentialProvider credentialProvider,
     IFileAccessService fileService
     ) : base(fileService)
 {
     this.document           = document ?? throw new ArgumentNullException(nameof(document));
     this.databaseFile       = databaseFile;
     this.credentialProvider = credentialProvider ?? throw new ArgumentNullException(nameof(credentialProvider));
 }
示例#10
0
 /// <summary>
 /// Creates a ViewModel wrapping a brand new KdbxGroup as a child of the specified parent group.
 /// </summary>
 /// <param name="navigationViewModel">A ViewModel used for tracking navigation history.</param>
 /// <param name="persistenceService">A service used for persisting the document.</param>
 /// <param name="document">A KdbxDocument representing the database we are working on.</param>
 /// <param name="parentGroup">The IKeePassGroup to use as a parent for the new group.</param>
 public GroupDetailsViewModel(
     IDatabaseNavigationViewModel navigationViewModel,
     IDatabasePersistenceService persistenceService,
     KdbxDocument document,
     IKeePassGroup parentGroup
     ) : base(navigationViewModel, persistenceService, document, new KdbxGroup(parentGroup), true, false)
 {
     if (parentGroup == null)
     {
         throw new ArgumentNullException("parentGroup");
     }
 }
示例#11
0
 /// <summary>
 /// Creates a ViewModel wrapping an existing KdbxGroup.
 /// <param name="navigationViewModel">A ViewModel used for tracking navigation history.</param>
 /// <param name="persistenceService">A service used for persisting the document.</param>
 /// <param name="document">A KdbxDocument representing the database we are working on.</param>
 /// <param name="groupToEdit">The group being viewed.</param>
 /// <param name="isReadOnly">Whether to open the group in read-only mode.</param>
 public GroupDetailsViewModel(
     IDatabaseNavigationViewModel navigationViewModel,
     IDatabasePersistenceService persistenceService,
     KdbxDocument document,
     IKeePassGroup groupToEdit,
     bool isReadOnly
     ) : base(navigationViewModel, persistenceService, document, groupToEdit, false, isReadOnly)
 {
     if (groupToEdit == null)
     {
         throw new ArgumentNullException("groupToEdit");
     }
 }
示例#12
0
        /// <summary>
        /// Initializes the EventArgs with the provided parameters.
        /// </summary>
        /// <param name="document">A model representing the decrypted XML document.</param>
        /// <param name="candidate">The file corresponding to the opened document.</param>
        /// <param name="persistenceService">A service that can persist the document.</param>
        /// <param name="rng">A random number generator that can encrypt protected strings for the document.</param>
        /// <param name="keyChangeVmFactory">Factory used to generate the value of <see cref="KeyChangeViewModel"/>.</param>
        public DocumentReadyEventArgs(
            KdbxDocument document,
            IDatabaseCandidate candidate,
            IDatabasePersistenceService persistenceService,
            IRandomNumberGenerator rng,
            IMasterKeyChangeViewModelFactory keyChangeVmFactory)
        {
            Document           = document;
            Candidate          = candidate;
            PersistenceService = persistenceService;
            Rng = rng;

            KeyChangeViewModel = keyChangeVmFactory?.Assemble(document, PersistenceService, candidate.File) ?? throw new ArgumentNullException(nameof(keyChangeVmFactory));
        }
示例#13
0
        public async Task Initialize()
        {
            Utils.DatabaseInfo databaseInfo = await Utils.GetDatabaseInfoForTest(TestContext);

            KdbxReader reader = new KdbxReader();

            using (IRandomAccessStream stream = await databaseInfo.Database.AsIStorageFile.OpenReadAsync())
            {
                Assert.IsFalse((await reader.ReadHeaderAsync(stream, CancellationToken.None)).IsError);
                KdbxDecryptionResult decryption = await reader.DecryptFileAsync(stream, databaseInfo.Password, databaseInfo.Keyfile, CancellationToken.None);

                Assert.IsFalse(decryption.Result.IsError);
                this.document = decryption.GetDocument();
                this.rng      = reader.HeaderData.GenerateRng();
            }
        }
示例#14
0
 /// <summary>
 /// Creates a ViewModel wrapping an existing KdbxGroup.
 /// </summary>
 /// <param name="resourceProvider">IResourceProvider for localizing strings.</param>
 /// <param name="navigationViewModel">A ViewModel used for tracking navigation history.</param>
 /// <param name="persistenceService">A service used for persisting the document.</param>
 /// <param name="clipboardService">A service used for accessing the clipboard.</param>
 /// <param name="settingsService">A service used to access app settings.</param>
 /// <param name="document">A KdbxDocument representing the database we are working on.</param>
 /// <param name="entryToEdit">The entry being viewed.</param>
 /// <param name="isReadOnly">Whether to open the group in read-only mode.</param>
 /// <param name="rng">A random number generator used to protect strings in memory.</param>
 public EntryDetailsViewModel(
     IResourceProvider resourceProvider,
     IDatabaseNavigationViewModel navigationViewModel,
     IDatabasePersistenceService persistenceService,
     ISensitiveClipboardService clipboardService,
     IAppSettingsService settingsService,
     KdbxDocument document,
     IKeePassEntry entryToEdit,
     bool isReadOnly,
     IRandomNumberGenerator rng
     ) : this(resourceProvider, navigationViewModel, persistenceService, clipboardService, settingsService, document, entryToEdit, false, isReadOnly, rng)
 {
     if (entryToEdit == null)
     {
         throw new ArgumentNullException(nameof(entryToEdit));
     }
 }
示例#15
0
        private async Task ExpectUnlockError(KdbxParserCode error, bool expectIdentical = true)
        {
            CancellationTokenSource cts    = new CancellationTokenSource();
            KdbxDecryptionResult    result = await this.reader.DecryptFileAsync(await this.thisTestInfo.Database.AsIStorageFile.OpenReadAsync(), this.thisTestInfo.Password, this.thisTestInfo.Keyfile, cts.Token);

            if (result.Result == ReaderResult.Success)
            {
                KdbxDocument oldDocument = result.GetDocument();
                XElement     newXml      = oldDocument.ToXml(this.reader.HeaderData.GenerateRng(), result.Parameters);
                KdbxDocument newDocument = new KdbxDocument(
                    newXml,
                    this.reader.HeaderData.ProtectedBinaries,
                    this.reader.HeaderData.GenerateRng(),
                    result.Parameters
                    );

                Assert.AreEqual(oldDocument, newDocument);
            }

            Assert.AreEqual(error, result.Result.Code);
        }
示例#16
0
        /// <summary>
        /// Initializes the instance.
        /// </summary>
        /// <param name="syncContext">Context to use for marshalling to the UI thread.</param>
        /// <param name="timerFactory">Used to create a timer.</param>
        /// <param name="file">The file on disk represented by this database.</param>
        /// <param name="fileIsSample">Whether this file is a sample file.</param>
        /// <param name="document">The decrypted database.</param>
        /// <param name="resourceProvider">A IResourceProvider for the View.</param>
        /// <param name="rng">A random number generator used to protect strings.</param>
        /// <param name="navigationViewModel">A ViewModel representing the navigation of the database.</param>
        /// <param name="masterKeyViewModel">A ViewModel that allows configuring the database's master key.</param>
        /// <param name="persistenceService">A service used to save the database.</param>
        /// <param name="identityService">A service used to authenticate the user.</param>
        /// <param name="credentialStorage">A service used to update saved credentials.</param>
        /// <param name="settingsService">A service used to access app settings.</param>
        /// <param name="clipboardService">A service used to access the clipboard for credentials.</param>
        public DatabaseParentViewModel(
            ISyncContext syncContext,
            ITimerFactory timerFactory,
            ITestableFile file,
            bool fileIsSample,
            KdbxDocument document,
            IResourceProvider resourceProvider,
            IRandomNumberGenerator rng,
            IDatabaseNavigationViewModel navigationViewModel,
            IMasterKeyViewModel masterKeyViewModel,
            IDatabasePersistenceService persistenceService,
            IIdentityVerificationService identityService,
            ICredentialStorageProvider credentialStorage,
            IAppSettingsService settingsService,
            ISensitiveClipboardService clipboardService
            ) : base(document, persistenceService)
        {
            if (timerFactory == null)
            {
                throw new ArgumentNullException(nameof(timerFactory));
            }

            this.syncContext = syncContext ?? throw new ArgumentNullException(nameof(syncContext));
            this.idleTimer   = timerFactory.Assemble(TimeSpan.FromSeconds(1));

            this.file             = file ?? throw new ArgumentNullException(nameof(file));
            this.fileIsSample     = fileIsSample;
            this.document         = document ?? throw new ArgumentNullException(nameof(document));
            this.resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider));
            this.rng = rng ?? throw new ArgumentNullException(nameof(rng));
            this.navigationViewModel = navigationViewModel ?? throw new ArgumentNullException(nameof(navigationViewModel));
            this.settingsViewModel   = new DatabaseSettingsViewModel(PersistenceService.SettingsProvider);
            this.masterKeyViewModel  = masterKeyViewModel ?? throw new ArgumentNullException(nameof(masterKeyViewModel));
            this.identityService     = identityService ?? throw new ArgumentNullException(nameof(identityService));
            this.credentialProvider  = credentialStorage ?? throw new ArgumentNullException(nameof(credentialStorage));
            this.settingsService     = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
            this.clipboardService    = clipboardService ?? throw new ArgumentNullException(nameof(clipboardService));
        }
示例#17
0
        public async Task Initialize()
        {
            try
            {
                CancellationTokenSource cts = new CancellationTokenSource();

                Utils.DatabaseInfo databaseInfo = await Utils.GetDatabaseInfoForTest(TestContext);

                KdbxReader reader = new KdbxReader();

                using (IRandomAccessStream stream = await databaseInfo.Database.AsIStorageFile.OpenReadAsync())
                {
                    Assert.IsFalse((await reader.ReadHeaderAsync(stream, cts.Token)).IsError);
                    KdbxDecryptionResult decryption = await reader.DecryptFileAsync(stream, databaseInfo.Password, databaseInfo.Keyfile, cts.Token);

                    Assert.IsFalse(decryption.Result.IsError);
                    this.document = decryption.GetDocument();
                }
            }
            catch (InvalidOperationException) { }

            this.viewModel = new DatabaseNavigationViewModel();
        }
示例#18
0
        private async Task RaiseDocumentReady(KdbxDocument document, IDatabaseCandidate candidate)
        {
            DebugHelper.Assert(HasGoodHeader);
            if (!HasGoodHeader)
            {
                throw new InvalidOperationException("Document cannot be ready, because the KdbxReader does not have good HeaderData.");
            }

            IDatabasePersistenceService persistenceService;

            if (IsSampleFile)
            {
                persistenceService = new DummyPersistenceService();
            }
            else
            {
                IKdbxWriter writer = this.kdbxReader.GetWriter();
                persistenceService = new DefaultFilePersistenceService(
                    writer,
                    writer,
                    candidate,
                    this.syncContext,
                    await candidate.File.CheckWritableAsync()
                    );
            }

            DocumentReady?.Invoke(
                this,
                new DocumentReadyEventArgs(
                    document,
                    candidate,
                    persistenceService,
                    this.kdbxReader.HeaderData.GenerateRng(),
                    this.keyChangeVmFactory
                    )
                );
        }
示例#19
0
        // Internal constructor for initializing fields and checking edge cases
        private KdbxDecryptionResult(ReaderResult error, KdbxSerializationParameters kdbxParameters, KdbxDocument document, IBuffer rawKey)
        {
            DebugHelper.Assert(error != null);
            if (error == null)
            {
                throw new ArgumentNullException(nameof(error));
            }

            if (error != ReaderResult.Success)
            {
                DebugHelper.Assert(document == null);
                if (document != null)
                {
                    throw new ArgumentException("If error is defined, the other arguments must be null");
                }
            }
            else
            {
                // Result is guaranteed to be Success at this point
                DebugHelper.Assert(document != null);
                if (document == null)
                {
                    throw new ArgumentNullException(nameof(document));
                }

                if (rawKey == null)
                {
                    throw new ArgumentNullException(nameof(rawKey));
                }
            }

            Result = error;
            this.kdbxParameters = kdbxParameters;
            this.kdbxDocument   = document;
            this.rawKey         = rawKey;
        }
示例#20
0
        /// <summary>
        /// Initializes the base class from a subclass.
        /// </summary>
        /// <param name="navigationViewModel">A ViewModel used for tracking navigation history.</param>
        /// <param name="persistenceService">A service used for persisting the document.</param>
        /// <param name="document">A KdbxDocument representing the database we are working on.</param>
        /// <param name="item">The item this ViewModel wraps.</param>
        /// <param name="isNew">Whether the child is being created for the first time.</param>
        /// <param name="isReadOnly">Whether the child is being accessed as read-only.</param>
        protected NodeDetailsViewModel(
            IDatabaseNavigationViewModel navigationViewModel,
            IDatabasePersistenceService persistenceService,
            KdbxDocument document,
            T item,
            bool isNew,
            bool isReadOnly
            ) : base(document, persistenceService)
        {
            if (isNew && item.Parent == null)
            {
                throw new ArgumentException("Cannot create a new node with no parent!");
            }

            if (navigationViewModel.ActiveGroup != item.Parent)
            {
                throw new ArgumentException("The database's active group must be the node's parent!");
            }

            NavigationViewModel = navigationViewModel ?? throw new ArgumentNullException(nameof(navigationViewModel));
            Document            = document ?? throw new ArgumentNullException(nameof(document));
            IsNew = isNew;

            if (!isNew)
            {
                this.masterCopy = GetClone(item);
                WorkingCopy     = GetClone(this.masterCopy);
            }
            else
            {
                this.masterCopy = null;
                WorkingCopy     = GetClone(item);
            }

            IsReadOnly = isReadOnly;
        }
示例#21
0
 /// <summary>
 /// Generats a ViewModel representing an existing child in the DOM.
 /// </summary>
 /// <param name="navigationViewModel"></param>
 /// <param name="persistenceService"></param>
 /// <param name="document"></param>
 /// <param name="openForReadOnly"></param>
 /// <returns>A ViewModel representing an existing child in the DOM.</returns>
 protected abstract TViewModel GetExistingViewModel(
     IDatabaseNavigationViewModel navigationViewModel,
     IDatabasePersistenceService persistenceService,
     KdbxDocument document,
     bool openForReadOnly
     );
示例#22
0
        /// <summary>
        /// Writes a document to the specified stream.
        /// </summary>
        /// <param name="file">The stream to write to.</param>
        /// <param name="document">The document to write.</param>
        /// <param name="token">A token allowing the operation to be cancelled.</param>
        /// <returns>Whether the write succeeded.</returns>
        public async Task <bool> WriteAsync(IOutputStream stream, KdbxDocument document, CancellationToken token)
        {
            DebugHelper.Assert(stream != null);
            if (stream == null)
            {
                throw new ArgumentNullException(nameof(stream));
            }

            HeaderData.ProtectedBinaries.Clear();
            foreach (ProtectedBinary bin in document?.Metadata?.Binaries?.Binaries ?? Enumerable.Empty <ProtectedBinary>())
            {
                HeaderData.ProtectedBinaries.Add(bin);
            }

            using (DataWriter writer = new DataWriter(stream))
            {
                // Configure the DataWriter
                writer.UnicodeEncoding = UnicodeEncoding.Utf8;
                writer.ByteOrder       = ByteOrder.LittleEndian;

                // Write the header in-memory first, so that we can generate the header hash.
                // This is because the header hash is the first thing written into the header.
                using (InMemoryRandomAccessStream headerStream = new InMemoryRandomAccessStream())
                {
                    using (DataWriter headerWriter = new DataWriter(headerStream)
                    {
                        UnicodeEncoding = UnicodeEncoding.Utf8, ByteOrder = ByteOrder.LittleEndian
                    })
                    {
                        WriteSignature(headerWriter);
                        WriteVersion(headerWriter);
                        await headerWriter.StoreAsync();

                        await WriteOuterHeaderAsync(headerWriter);

                        await headerWriter.StoreAsync();

                        headerWriter.DetachStream();
                    }

                    // Seek to the start of this temporary stream, so we can hash what we have.
                    headerStream.Seek(0);
                    using (DataReader headerReader = new DataReader(headerStream)
                    {
                        UnicodeEncoding = UnicodeEncoding.Utf8, ByteOrder = ByteOrder.LittleEndian
                    })
                    {
                        await headerReader.LoadAsync((uint)headerStream.Size);

                        HeaderData.FullHeader = headerReader.ReadBuffer((uint)headerStream.Size);

                        var sha256             = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256);
                        CryptographicHash hash = sha256.CreateHash();
                        hash.Append(HeaderData.FullHeader);

                        IBuffer hashedHeaderBuffer = hash.GetValueAndReset();
                        HeaderData.HeaderHash        = CryptographicBuffer.EncodeToBase64String(hashedHeaderBuffer);
                        document.Metadata.HeaderHash = HeaderData.HeaderHash;

                        XDocument xmlDocument = new XDocument(document.ToXml(HeaderData.GenerateRng(), this.parameters));
                        try
                        {
                            this.rawKey = this.rawKey ?? await KeyHelper.GetRawKey(this.securityTokens);

                            IBuffer transformedKey = await HeaderData.KdfParameters.CreateEngine().TransformKeyAsync(this.rawKey, token);

                            if (transformedKey == null)
                            {
                                throw new OperationCanceledException();
                            }

                            DebugHelper.Trace("Got transformed k from KDF.");

                            token.ThrowIfCancellationRequested();

                            writer.WriteBuffer(HeaderData.FullHeader);
                            await writer.StoreAsync();

                            token.ThrowIfCancellationRequested();

                            // In KDBX4, after the header is an HMAC-SHA-256 value computed over the header
                            // allowing validation of header integrity.
                            IBuffer          hmacKey     = HmacBlockHandler.DeriveHmacKey(transformedKey, HeaderData.MasterSeed);
                            HmacBlockHandler hmacHandler = new HmacBlockHandler(hmacKey);

                            if (this.parameters.UseInlineHeaderAuthentication)
                            {
                                // Write plain hash, followed by HMAC
                                writer.WriteBuffer(hashedHeaderBuffer);

                                var algorithm = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithmNames.HmacSha256);
                                CryptographicHash hmacHash = algorithm.CreateHash(hmacHandler.GetKeyForBlock(UInt64.MaxValue));
                                hmacHash.Append(HeaderData.FullHeader);

                                IBuffer headerMac = hmacHash.GetValueAndReset();
                                writer.WriteBuffer(headerMac);

                                token.ThrowIfCancellationRequested();
                            }

                            // Write the encrypted content that comes after the header
                            // For KDBX3 this is the database, for KDBX4 it includes the inner header
                            IBuffer cipherText = await GetCipherTextAsync(xmlDocument, transformedKey, token);

                            if (this.parameters.UseHmacBlocks)
                            {
                                uint blockCount = (cipherText.Length + HmacBlockHandler.BlockSize - 1) / HmacBlockHandler.BlockSize;
                                for (uint i = 0; i < blockCount; i++)
                                {
                                    await hmacHandler.WriteCipherBlockAsync(writer, cipherText, i *HmacBlockHandler.BlockSize, HmacBlockHandler.BlockSize, i);
                                }

                                // We signal we're done by writing an empty HMAC "terminator" block
                                await hmacHandler.WriteTerminatorAsync(writer, blockCount);
                            }
                            else
                            {
                                writer.WriteBuffer(cipherText);
                                await writer.StoreAsync();
                            }
                        }
                        catch (OperationCanceledException)
                        {
                            return(false);
                        }
                    }
                }

                await stream.FlushAsync();

                writer.DetachStream();
                return(true);
            }
        }
示例#23
0
 /// <summary>
 /// Replaces a child within its parent's collection.
 /// </summary>
 /// <param name="document">The document being updated.</param>
 /// <param name="parent">The parent to update.</param>
 /// <param name="child">The child to use as a replacement.</param>
 /// <param name="touchesNode">Whether to treat the swap as an "update" (vs a revert).</param>
 protected abstract void SwapIntoParent(KdbxDocument document, IKeePassGroup parent, T child, bool touchesNode);
示例#24
0
        /// <summary>
        /// Uses provided options to generate a database file.
        /// </summary>
        protected override async Task HandleCredentialsAsync(string confirmedPassword, ITestableFile chosenKeyFile)
        {
            CancellationTokenSource cts = new CancellationTokenSource();

            IKdbxWriter writer = this.writerFactory.Assemble(
                confirmedPassword,
                chosenKeyFile,
                Settings.Cipher,
                Settings.GetKdfParameters()
                );
            IRandomNumberGenerator rng = writer.HeaderData.GenerateRng();

            KdbxDocument newDocument = new KdbxDocument(new KdbxMetadata("PassKeep Database"));

            if (!CreateEmpty)
            {
                IList <IKeePassGroup> groups = new List <IKeePassGroup>
                {
                    new KdbxGroup(newDocument.Root.DatabaseGroup),
                    new KdbxGroup(newDocument.Root.DatabaseGroup),
                    new KdbxGroup(newDocument.Root.DatabaseGroup),
                    new KdbxGroup(newDocument.Root.DatabaseGroup),
                    new KdbxGroup(newDocument.Root.DatabaseGroup),
                    new KdbxGroup(newDocument.Root.DatabaseGroup)
                };

                groups[0].Title.ClearValue = "General";
                groups[1].Title.ClearValue = "Windows";
                groups[2].Title.ClearValue = "Network";
                groups[3].Title.ClearValue = "Internet";
                groups[4].Title.ClearValue = "eMail";
                groups[5].Title.ClearValue = "Homebanking";

                groups[0].IconID = 48;
                groups[1].IconID = 38;
                groups[2].IconID = 3;
                groups[3].IconID = 1;
                groups[4].IconID = 19;
                groups[5].IconID = 37;

                foreach (IKeePassGroup group in groups)
                {
                    newDocument.Root.DatabaseGroup.Children.Add(group);
                }

                IList <IKeePassEntry> entries = new List <IKeePassEntry>
                {
                    new KdbxEntry(newDocument.Root.DatabaseGroup, rng, newDocument.Metadata),
                    new KdbxEntry(newDocument.Root.DatabaseGroup, rng, newDocument.Metadata)
                };

                entries[0].Title.ClearValue = "Sample Entry";
                entries[1].Title.ClearValue = "Sample Entry #2";

                entries[0].UserName.ClearValue = "User Name";
                entries[1].UserName.ClearValue = "Michael321";

                entries[0].Password.ClearValue = "Password";
                entries[1].Password.ClearValue = "12345";

                entries[0].Url.ClearValue = "http://keepass.info/";
                entries[1].Url.ClearValue = "http://keepass.info/help/kb/testform.html";

                entries[0].Notes.ClearValue = "Notes";

                foreach (IKeePassEntry entry in entries)
                {
                    newDocument.Root.DatabaseGroup.Children.Add(entry);
                }
            }

            using (IRandomAccessStream stream = await File.AsIStorageFile.OpenAsync(FileAccessMode.ReadWrite))
            {
                Task <bool> writeTask = writer.WriteAsync(stream, newDocument, cts.Token);
                this.taskNotificationService.PushOperation(writeTask, cts, AsyncOperationType.DatabaseEncryption);

                if (await writeTask)
                {
                    this.futureAccessList.Add(File, File.AsIStorageItem.Name);

                    IDatabaseCandidate candidate = await this.candidateFactory.AssembleAsync(File);

                    IDatabasePersistenceService persistenceService = new DefaultFilePersistenceService(
                        writer,
                        writer,
                        candidate,
                        this.syncContext,
                        true);

                    DocumentReady?.Invoke(
                        this,
                        new DocumentReadyEventArgs(
                            newDocument,
                            candidate,
                            persistenceService,
                            writer.HeaderData.GenerateRng(),
                            this.keyChangeVmFactory
                            )
                        );
                }
            }
        }
示例#25
0
 /// <summary>
 /// Does nothing.
 /// </summary>
 /// <param name="document">The KdbxDocument to persist.</param>
 /// <returns>A Task that will evaluate to true.</returns>
 public Task <bool> Save(KdbxDocument document) => Task.FromResult(true);
        /// <summary>
        /// Attempts to asynchronously persist the document to its default location.
        /// If a save is already in progress, it is cancelled and the more recent save should
        /// override it.
        /// </summary>
        /// <param name="document">The KdbxDocument to persist.</param>
        /// <returns>A Task representing whether the save was successful.</returns>
        public async Task <bool> Save(KdbxDocument document)
        {
            if (document == null)
            {
                throw new ArgumentNullException(nameof(document));
            }

            if (!CanSave)
            {
                return(false);
            }

            // Lock to avoid a race condition between checking not null and cancelling
            bool firePropertyChanged = false;

            lock (this.ctsLock)
            {
                if (IsSaving)
                {
                    this.currentSaveCts.Cancel();
                }
                else
                {
                    // We only fire PropertyChanged for false -> true if the
                    // transition is actually happening. If we are pre-empting
                    // a save in progress, then it is not useful to fire the
                    // event.
                    firePropertyChanged = true;
                }

                this.pendingRequests++;
            }

            // Cancelling above may cause a previous save to wrap up faster, but we do still need
            // to wait for it to wrap up.
            await this.saveSemaphore.WaitAsync();

            // Inside the semaphore it is impossible for a save to already be in progress.
            // This is because we clean up the current save at the end of the semaphore, so if
            // we just entered it, there is no pending operation.
            // However, we still want to lock around the CTS in case another save starts right
            // away and needs to cancel this one.
            lock (this.ctsLock)
            {
                DebugHelper.Assert(this.currentSaveCts == null);

                this.pendingRequests--;
                if (this.pendingRequests == 0)
                {
                    // If pendingRequests > 0, then at least one more recent call to
                    // Save is currently stalled at the semaphore.
                    // We only kick off this save if that's NOT true.
                    this.currentSaveCts = new CancellationTokenSource();
                }
            }

            // Only proceed with this save attempt if there are no pending requests...
            // otherwise this block is skipped and we immediately release the semaphore
            // and return false.
            bool writeResult = false;

            if (this.currentSaveCts != null)
            {
                if (firePropertyChanged)
                {
#pragma warning disable CS4014 // No need to await this to continue saving.
                    this.syncContext.Post(() => OnPropertyChanged(nameof(IsSaving)));
#pragma warning restore CS4014
                }

                // Do the write to a temporary file until it's finished successfully.
                StorageFile outputFile = await GetTemporaryFile();

                using (IRandomAccessStream fileStream = await outputFile.OpenAsync(FileAccessMode.ReadWrite))
                {
                    using (IOutputStream outputStream = fileStream.GetOutputStreamAt(0))
                    {
                        writeResult = await this.fileWriter.WriteAsync(fileStream, document, this.currentSaveCts.Token);
                    }
                }

                if (writeResult)
                {
                    Task replaceTask = this.defaultSaveFile.ReplaceWithAsync(outputFile);
                    try
                    {
                        await replaceTask;
                    }
                    catch (Exception)
                    {
                    }
                }

                try
                {
                    // Make a good-faith effort to delete the temp file, due
                    // to reports that Windows might not handle this automatically.
                    await outputFile.DeleteAsync();
                }
                catch (Exception e)
                {
                    DebugHelper.Trace($"Caught exception during temp file cleanup: {e}");
                }

                // At this point we are done with all file IO - clean up and let any
                // pending saves do their thing.
                firePropertyChanged = false;
                lock (this.ctsLock)
                {
                    this.currentSaveCts.Dispose();
                    this.currentSaveCts = null;
                    if (this.pendingRequests == 0)
                    {
                        firePropertyChanged = true;
                    }
                }

                // We only update IsSaving if nothing else is pending - if another save
                // is already queued, it's just going to flip this back to true immediately.
                if (firePropertyChanged)
                {
#pragma warning disable CS4014 // No need to await this to continue saving.
                    this.syncContext.Post(() => OnPropertyChanged(nameof(IsSaving)));
#pragma warning restore CS4014
                }
            }

            this.saveSemaphore.Release();

            return(writeResult);
        }
示例#27
0
        /// <summary>
        /// Passes provided parameters to the base constructor and initializes commands.
        /// </summary>
        /// <param name="resourceProvider">IResourceProvider for localizing strings.</param>
        /// <param name="navigationViewModel"></param>
        /// <param name="persistenceService"></param>
        /// <param name="clipboardService"></param>
        /// <param name="settingsService"></param>
        /// <param name="document"></param>
        /// <param name="entry"></param>
        /// <param name="isNew"></param>
        /// <param name="isReadOnly"></param>
        /// <param name="rng"></param>
        private EntryDetailsViewModel(
            IResourceProvider resourceProvider,
            IDatabaseNavigationViewModel navigationViewModel,
            IDatabasePersistenceService persistenceService,
            ISensitiveClipboardService clipboardService,
            IAppSettingsService settingsService,
            KdbxDocument document,
            IKeePassEntry entry,
            bool isNew,
            bool isReadOnly,
            IRandomNumberGenerator rng
            ) : base(navigationViewModel, persistenceService, document, entry, isNew, isReadOnly)
        {
            this.resourceProvider = resourceProvider;
            this.clipboardService = clipboardService;
            this.settingsService  = settingsService;
            this.rng = rng;

            this.copyFieldValueCommand = new TypedCommand <IProtectedString>(
                str =>
            {
                clipboardService.CopyCredential(str.ClearValue, ClipboardOperationType.Other);
            }
                );

            this.deleteFieldCommand = new TypedCommand <IProtectedString>(
                str => !IsReadOnly && PersistenceService.CanSave,
                str =>
            {
                DebugHelper.Assert(!IsReadOnly);
                WorkingCopy.Fields.Remove(str);
            }
                );

            this.editFieldCommand = new AsyncTypedCommand <IProtectedString>(
                str => PersistenceService.CanSave,
                async str =>
            {
                IsReadOnly = false;
                await UpdateFieldEditorViewModel(new FieldEditorViewModel(str, this.resourceProvider));
            }
                );

            this.newFieldCommand = new AsyncActionCommand(
                () => PersistenceService.CanSave,
                async() =>
            {
                IsReadOnly = false;
                await UpdateFieldEditorViewModel(new FieldEditorViewModel(this.rng, this.resourceProvider));
            }
                );

            this.commitFieldCommand = new AsyncActionCommand(
                () => FieldEditorViewModel?.CommitCommand.CanExecute(WorkingCopy) ?? false,
                async() =>
            {
                FieldEditorViewModel.CommitCommand.Execute(WorkingCopy);
                await UpdateFieldEditorViewModel(null);
            }
                );

            PropertyChanged += (s, e) =>
            {
                if (e.PropertyName == nameof(IsReadOnly))
                {
                    ((TypedCommand <IProtectedString>)DeleteFieldCommand).RaiseCanExecuteChanged();
                }
                else if (e.PropertyName == nameof(WorkingCopy))
                {
                    OnPropertyChanged(nameof(WorkingCopyViewModel));
                }
            };
        }
示例#28
0
        /// <summary>
        /// Asynchronously attempts to unlock the document file.
        /// </summary>
        /// <remarks>
        /// Algorithm is as of this writing (11/5/2012):
        /// 0. Use UTF8 encoding with no BOM.
        /// 1. Read header.
        /// 2. Compute SHA256 hash of header.
        /// 3. Decrypt the rest of the viewModel using header parameters.
        ///     Relies on:
        ///         a. MasterSeed.Length == 32
        ///             Write masterseed to stream
        ///         b. GenerateKey32(_transformSeed, KeyEncryptionRounds)
        ///             Create raw32 (CreateRawCompositeKey32)
        ///                 Concatenate all data and Sha256
        ///             TransformKey(raw32, _transformSeed, numRounds)
        ///                 Init Rijndael:
        ///                     128 bit (16 byte) blocks
        ///                     ECB mode
        ///                     k = _transformSeed
        ///                     For numRounds:
        ///                         Transform in place raw32[0:15]
        ///                         Transform in place raw32[16:31]
        ///         c. Write 32 bytes of Key32 to stream
        ///         d. aesKey = Sha256 the stream
        ///         e. DecryptStream with aesKey and _encryptionIV
        /// 4. Verify the first 32 bytes of the decrypted viewModel match up with
        ///     "StreamStartBytes" from the header.
        /// 5. Read from the decrypted viewModel as a "HashedBlockStream"
        ///
        /// File format at the time of this writing (11/5/2012):
        ///
        /// 4 bytes: SIG1
        /// 4 bytes: SIG2
        /// Failure to match these constants results in a parse Result.
        ///
        /// 4 bytes: File version
        ///
        /// Header fields:
        /// 1 byte: Field ID
        /// 2 bytes: Field size (n)
        /// n bytes: Data
        /// </remarks>
        /// <param name="stream">An IRandomAccessStream containing the document to unlock (including the header).</param>
        /// <param name="rawKey">The aggregate raw key to use for decrypting the database.</param>
        /// <param name="token">A token allowing the parse to be cancelled.</param>
        /// <returns>A Task representing the result of the descryiption operation.</returns>
        public async Task <KdbxDecryptionResult> DecryptFile(IRandomAccessStream stream, IBuffer rawKey, CancellationToken token)
        {
            if (HeaderData == null)
            {
                throw new InvalidOperationException("Cannot decrypt database before ReadHeader has been called.");
            }

            // Init a SHA256 hash buffer and append the master seed to it
            HashAlgorithmProvider sha256 = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256);
            CryptographicHash     hash   = sha256.CreateHash();

            hash.Append(HeaderData.MasterSeed);

            this.rawKey = rawKey;
            this.logger.LogEvent("KdbxReader.GotRawKey", EventVerbosity.Verbose);

            // Transform the key (this can take a while)
            IBuffer transformedKey;

            try
            {
                IKdfEngine kdf = HeaderData.KdfParameters.CreateEngine();

                LoggingFields fields = new LoggingFields();
                fields.AddString("KdfEngine", kdf.GetType().Name);
                this.logger.LogEvent("KdbxReader.StartingKeyTransform", fields, EventVerbosity.Info);

                transformedKey = await HeaderData.KdfParameters.CreateEngine().TransformKeyAsync(rawKey, token)
                                 .ConfigureAwait(false);

                if (transformedKey == null)
                {
                    throw new OperationCanceledException();
                }

                this.logger.LogEvent("KdbxReader.KeyTransformSucceeded", EventVerbosity.Info);
            }
            catch (OperationCanceledException)
            {
                return(new KdbxDecryptionResult(new ReaderResult(KdbxParserCode.OperationCancelled)));
            }

            // In KDBX4, after the header is an HMAC-SHA-256 value computed over the header
            // allowing validation of header integrity.
            IBuffer          hmacKey     = HmacBlockHandler.DeriveHmacKey(transformedKey, HeaderData.MasterSeed);
            HmacBlockHandler hmacHandler = new HmacBlockHandler(hmacKey);

            IBuffer expectedMac = null;

            if (this.parameters.UseInlineHeaderAuthentication)
            {
                var algorithm = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithmNames.HmacSha256);
                CryptographicHash hmacHash = algorithm.CreateHash(hmacHandler.GetKeyForBlock(UInt64.MaxValue));

                DebugHelper.Assert(HeaderData.FullHeader != null);
                hmacHash.Append(HeaderData.FullHeader);

                expectedMac = hmacHash.GetValueAndReset();
            }

            // Hash transformed k (with the master seed) to get final cipher k
            hash.Append(transformedKey);
            IBuffer cipherKey = hash.GetValueAndReset();

            this.logger.LogEvent("KdbxReader.GotFinalCipherKey", EventVerbosity.Info);

            // Decrypt the document starting from the end of the header
            ulong headerLength = HeaderData.FullHeader.Length;

            if (this.parameters.UseInlineHeaderAuthentication)
            {
                // KDBX4 has a hash at the end of the header
                headerLength += 32;
            }

            stream.Seek(headerLength);
            if (expectedMac != null)
            {
                using (DataReader macReader = GetReaderForStream(stream))
                {
                    await macReader.LoadAsync(expectedMac.Length);

                    IBuffer actualMac = macReader.ReadBuffer(expectedMac.Length);

                    for (uint i = 0; i < expectedMac.Length; i++)
                    {
                        if (expectedMac.GetByte(i) != actualMac.GetByte(i))
                        {
                            this.logger.LogEvent("KdbxReader.HmacFailure", EventVerbosity.Critical);
                            return(new KdbxDecryptionResult(new ReaderResult(KdbxParserCode.CouldNotDecrypt)));
                        }
                    }

                    macReader.DetachStream();
                }
            }

            IBuffer cipherText;

            try
            {
                cipherText = await GetCipherText(stream, hmacHandler);
            }
            catch (FormatException ex)
            {
                this.logger.LogEvent("KdbxReader.DataIntegrityFailure", ex.ToLoggingFields(), EventVerbosity.Critical);
                return(new KdbxDecryptionResult(new ReaderResult(KdbxParserCode.DataIntegrityProblem, ex)));
            }

            IBuffer decryptedFile = DecryptDatabaseData(cipherText, cipherKey);

            if (decryptedFile == null)
            {
                this.logger.LogEvent("KdbxReader.DecryptionFailure", EventVerbosity.Critical);
                return(new KdbxDecryptionResult(new ReaderResult(KdbxParserCode.CouldNotDecrypt)));
            }

            this.logger.LogEvent("KdbxReader.DecryptionSucceeded", EventVerbosity.Info);

            // Verify first 32 bytes of the clear data; if StreamStartBytes wasn't set
            // (e.g. due to KDBX4), nothing happens here.
            for (uint i = 0; i < (HeaderData.StreamStartBytes?.Length ?? 0); i++)
            {
                byte actualByte   = decryptedFile.GetByte(i);
                byte expectedByte = HeaderData.StreamStartBytes.GetByte(i);

                if (actualByte != expectedByte)
                {
                    this.logger.LogEvent("KdbxReader.PlaintextValidationFailure", EventVerbosity.Critical);
                    return(new KdbxDecryptionResult(new ReaderResult(KdbxParserCode.FirstBytesMismatch)));
                }
            }

            this.logger.LogEvent("KdbxReader.PlaintextValidationSucceeded", EventVerbosity.Verbose);

            IBuffer plainText = await UnhashAndInflate(decryptedFile);

            if (plainText == null)
            {
                return(new KdbxDecryptionResult(new ReaderResult(KdbxParserCode.CouldNotInflate)));
            }

            // Update HeaderData with info from the inner header, if relevant
            if (this.parameters.UseInnerHeader)
            {
                using (IRandomAccessStream plainTextStream = plainText.AsStream().AsRandomAccessStream())
                {
                    using (DataReader reader = GetReaderForStream(plainTextStream))
                    {
                        ReaderResult innerHeaderResult = await ReadInnerHeader(reader, HeaderData);

                        if (innerHeaderResult != ReaderResult.Success)
                        {
                            LoggingFields fields = new LoggingFields();
                            fields.AddInt32("Code", (int)innerHeaderResult.Code);
                            this.logger.LogEvent("KdbxReader.InnerHeaderReadFailure", fields, EventVerbosity.Critical);
                            return(new KdbxDecryptionResult(innerHeaderResult));
                        }

                        // Update plainText to point to the remainder of the buffer
                        uint bytesRemaining = plainText.Length - (uint)plainTextStream.Position;
                        await reader.LoadAsync(bytesRemaining);

                        plainText = reader.ReadBuffer(bytesRemaining);
                    }
                }
            }

            XDocument finalTree = null;

            try
            {
                finalTree = XDocument.Load(plainText.AsStream());
            }
            catch (XmlException)
            {
                return(null);
            }

            if (finalTree == null)
            {
                return(new KdbxDecryptionResult(new ReaderResult(KdbxParserCode.MalformedXml)));
            }

            try
            {
                KdbxDocument parsedDocument = await Task.Run(() => new KdbxDocument(finalTree.Root, HeaderData.ProtectedBinaries, HeaderData.GenerateRng(), this.parameters));

                // Validate the final parsed header hash before returning
                if (this.parameters.UseXmlHeaderAuthentication &&
                    !String.IsNullOrEmpty(parsedDocument.Metadata.HeaderHash) &&
                    parsedDocument.Metadata.HeaderHash != HeaderData.HeaderHash)
                {
                    return(new KdbxDecryptionResult(new ReaderResult(KdbxParserCode.BadHeaderHash)));
                }

                return(new KdbxDecryptionResult(this.parameters, parsedDocument, this.rawKey));
            }
            catch (KdbxParseException e)
            {
                return(new KdbxDecryptionResult(e.Error));
            }
        }
示例#29
0
 /// <summary>
 /// Generates a ViewModel representing a new child.
 /// </summary>
 /// <param name="navigationViewModel"></param>
 /// <param name="persistenceService"></param>
 /// <param name="document"></param>
 /// <param name="parent"></param>
 /// <returns>A ViewModel representing a new child not already in the tree.</returns>
 protected abstract TViewModel GetNewViewModel(
     IDatabaseNavigationViewModel navigationViewModel,
     IDatabasePersistenceService persistenceService,
     KdbxDocument document,
     IKeePassGroup parent
     );
示例#30
0
 /// <summary>
 /// Initializes the ViewModel base.
 /// </summary>
 /// <param name="document">The document that will be saved.</param>
 /// <param name="persistenceService">The service to use for document writing.</param>
 protected DatabasePersistenceViewModel(KdbxDocument document, IDatabasePersistenceService persistenceService)
 {
     this.document      = document ?? throw new ArgumentNullException(nameof(document));
     PersistenceService = persistenceService ?? throw new ArgumentNullException(nameof(persistenceService));
 }