/// <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)); } }
/// <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); } }