Ejemplo n.º 1
0
        /// <summary>
        /// Given a stream past the header, asynchronously fetches encrypted ciphertext
        /// from the file as a single buffer.
        /// </summary>
        /// <remarks>
        /// For KDBX3.1, this is just the rest of the stream. For KDBX4, it involves
        /// pulling out HMAC blocks from the stream.
        /// </remarks>
        /// <param name="dataStream">The stream representing the remainder of the database file.</param>
        /// <returns>A buffer representing the encrypted database.</returns>
        private async Task <IBuffer> GetCipherText(IRandomAccessStream dataStream, HmacBlockHandler macHandler)
        {
            uint streamRemaining = (uint)(dataStream.Size - dataStream.Position);

            DebugHelper.Trace("Stream has {0} bytes remaining.", streamRemaining);

            IBuffer fileRemainder;

            using (DataReader reader = GetReaderForStream(dataStream))
            {
                if (this.parameters.UseHmacBlocks)
                {
                    // KDBX 4: HMAC block content
                    int bytesLeft = (int)streamRemaining;
                    DebugHelper.Assert(bytesLeft > 0);

                    fileRemainder = WindowsRuntimeBuffer.Create(bytesLeft);

                    for (ulong index = 0; bytesLeft > 0; index++)
                    {
                        IBuffer block = await macHandler.ReadCipherBlockAsync(reader, index);

                        if (block == null || block.Length == 0)
                        {
                            break;
                        }

                        DebugHelper.Assert((int)block.Length > 0);
                        bytesLeft -= (int)block.Length + 4 + 32;

                        block.CopyTo(0, fileRemainder, fileRemainder.Length, block.Length);
                        fileRemainder.Length += block.Length;
                    }
                }
                else
                {
                    // KDBX 3.1: Rest of file as-is
                    DebugHelper.Assert(reader.UnconsumedBufferLength == 0);
                    await reader.LoadAsync(streamRemaining).AsTask().ConfigureAwait(false);

                    fileRemainder = reader.ReadBuffer(streamRemaining);
                }

                reader.DetachStream();
                return(fileRemainder);
            }
        }
Ejemplo n.º 2
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));
            }
        }
Ejemplo n.º 3
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);
            }
        }