/// <summary> /// Asynchronously reads inner header values from a data reader. /// </summary> /// <param name="reader">Reader to pull data from.</param> /// <param name="headerData"><see cref="KdbxHeaderData"/> instance to populate with read values.</param> /// <returns>A task that resolves to a <see cref="ReaderResult"/> indicating whether reading was successful.</returns> public async Task <ReaderResult> ReadInnerHeader(DataReader reader, KdbxHeaderData headerData) { if (reader == null) { throw new ArgumentNullException(nameof(reader)); } bool gotEndOfHeader = false; while (!gotEndOfHeader) { try { InnerHeaderField field = await ReadInnerHeaderField(reader, headerData); if (field == InnerHeaderField.EndOfHeader) { gotEndOfHeader = true; } } catch (KdbxParseException e) { reader.DetachStream(); return(e.Error); } } return(ReaderResult.Success); }
/// <summary> /// Ensures the writer's serialization settings are correct for the given user preferences. /// </summary> /// <param name="cipher">The algorithm to use for encrypting the database.</param> /// <param name="rngAlgorithm">The random number generator used for String protection.</param> /// <param name="compression">The document compression algorithm.</param> /// <param name="kdfParams">Recipe for transforming the raw key. This will be reseeded.</param> private void SeedHeaderData( EncryptionAlgorithm cipher, RngAlgorithm rngAlgorithm, CompressionAlgorithm compression, KdfParameters kdfParams ) { if (kdfParams == null) { throw new ArgumentNullException(nameof(kdfParams)); } KdbxVersion version = KdbxVersion.Three; if (cipher == EncryptionAlgorithm.ChaCha20 || rngAlgorithm == RngAlgorithm.ChaCha20 || !kdfParams.Uuid.Equals(AesParameters.AesUuid)) { DebugHelper.Trace("Using KDBX4 for serialization due to header parameters"); version = KdbxVersion.Four; } this.parameters = new KdbxSerializationParameters(version) { Compression = compression }; // "Stream start bytes" are random data encrypted at the beginning // of the KDBX data block. They have been superceded by HMAC authentication. IBuffer streamStartBytes; if (this.parameters.UseHmacBlocks) { streamStartBytes = new byte[0].AsBuffer(); } else { streamStartBytes = CryptographicBuffer.GenerateRandom(32); } HeaderData = new KdbxHeaderData(KdbxHeaderData.Mode.Write) { Cipher = cipher, // This will automatically set EncryptionIV Compression = compression, MasterSeed = CryptographicBuffer.GenerateRandom(32), KdfParameters = kdfParams.Reseed(), StreamStartBytes = streamStartBytes, InnerRandomStreamKey = CryptographicBuffer.GenerateRandom(32).ToArray(), InnerRandomStream = rngAlgorithm }; }
/// <summary> /// Attempts to read and validate the next header field in the data stream. /// </summary> /// <param name="reader">A reader of the document file.</param> /// <param name="headerData">The header data that has been extracted so far.</param> /// <returns>A Task representing the field that was read.</returns> private async Task <OuterHeaderField> ReadOuterHeaderField(DataReader reader, KdbxHeaderData headerData) { // A header is guaranteed to have 3 (or 5) bytes at the beginning. // The first byte represents the type or ID of the header. // The next two (or four) bytes represent a 16-bit unsigned integer giving the size of the data field. uint sizeBytes = this.parameters.HeaderFieldSizeBytes; await reader.LoadAsync(1U + sizeBytes); // Read the header ID from the first byte var fieldId = (OuterHeaderField)reader.ReadByte(); // Read the header data field size from the next two bytes uint size; if (sizeBytes == 2) { size = reader.ReadUInt16(); } else { DebugHelper.Assert(sizeBytes == 4); size = reader.ReadUInt32(); } LoggingFields headerTraceFields = new LoggingFields(); headerTraceFields.AddString("FieldId", fieldId.ToString()); headerTraceFields.AddUInt32("Bytes", size); this.logger.LogEvent("KdbxReader.OuterHeaderField", headerTraceFields, EventVerbosity.Info); await reader.LoadAsync(size); // The cast above may have succeeded but still resulted in an unknown value (outside of the enum). // If so, we need to bail. if (!Enum.IsDefined(typeof(OuterHeaderField), fieldId)) { throw new KdbxParseException(ReaderResult.FromHeaderFieldUnknown((byte)fieldId)); } MemberInfo memberInfo = typeof(OuterHeaderField).GetMember(fieldId.ToString()) .FirstOrDefault(); DebugHelper.Assert(memberInfo != null); KdbxVersionSupportAttribute versionAttr = memberInfo.GetCustomAttribute <KdbxVersionSupportAttribute>(); if (versionAttr != null) { DebugHelper.Trace($"Found version attribute for header: {versionAttr}"); DebugHelper.Assert(versionAttr.Supports(this.parameters.Version)); } byte[] data = new byte[size]; reader.ReadBytes(data); // Based on the header field in question, the data is validated differently. // The size of the data field needs to be validated, and the data itself may need to be parsed. switch (fieldId) { case OuterHeaderField.EndOfHeader: break; case OuterHeaderField.CipherID: RequireFieldDataSizeEq(fieldId, 16, size); Guid cipherGuid = new Guid(data); if (cipherGuid.Equals(AesCipher.Uuid)) { headerData.Cipher = EncryptionAlgorithm.Aes; this.expectedIvBytes = AesCipher.IvBytes; } else if (cipherGuid.Equals(ChaCha20Cipher.Uuid)) { DebugHelper.Assert(this.parameters.Version == KdbxVersion.Four); headerData.Cipher = EncryptionAlgorithm.ChaCha20; this.expectedIvBytes = ChaCha20Cipher.IvBytes; } else { // If we get here, the cipher provided is not supported. throw new KdbxParseException(ReaderResult.FromHeaderDataUnknown(fieldId, cipherGuid.ToString())); } break; case OuterHeaderField.CompressionFlags: RequireFieldDataSizeEq(fieldId, 4, size); headerData.Compression = (CompressionAlgorithm)BitConverter.ToUInt32(data, 0); this.parameters.Compression = headerData.Compression; RequireEnumDefined(fieldId, headerData.Compression); break; case OuterHeaderField.MasterSeed: RequireFieldDataSizeEq(fieldId, 32, size); headerData.MasterSeed = CryptographicBuffer.CreateFromByteArray(data); break; case OuterHeaderField.TransformSeed: RequireFieldDataSizeEq(fieldId, 32, size); headerData.TransformSeed = CryptographicBuffer.CreateFromByteArray(data); break; case OuterHeaderField.TransformRounds: RequireFieldDataSizeEq(fieldId, 8, size); headerData.TransformRounds = BitConverter.ToUInt64(data, 0); break; case OuterHeaderField.EncryptionIV: RequireFieldDataSizeEq(fieldId, this.expectedIvBytes, size); headerData.EncryptionIV = CryptographicBuffer.CreateFromByteArray(data); break; case OuterHeaderField.ProtectedStreamKey: RequireFieldDataSize(fieldId, size, (n) => n > 0, "must be nonzero"); headerData.InnerRandomStreamKey = data; break; case OuterHeaderField.StreamStartBytes: headerData.StreamStartBytes = CryptographicBuffer.CreateFromByteArray(data); break; case OuterHeaderField.InnerRandomStreamID: RequireFieldDataSizeEq(fieldId, 4, size); headerData.InnerRandomStream = (RngAlgorithm)BitConverter.ToUInt32(data, 0); RequireEnumDefined(fieldId, headerData.InnerRandomStream); break; case OuterHeaderField.KdfParameters: try { using (IInputStream memStream = new MemoryStream(data).AsInputStream()) { using (DataReader vdReader = GetReaderForStream(memStream)) { VariantDictionary kdfParamDict = await VariantDictionary.ReadDictionaryAsync(vdReader); KdfParameters kdfParams = new KdfParameters(kdfParamDict); if (kdfParams.Uuid == AesParameters.AesUuid) { headerData.KdfParameters = new AesParameters(kdfParamDict); } else if (kdfParams.Uuid == Argon2Parameters.Argon2Uuid) { headerData.KdfParameters = new Argon2Parameters(kdfParamDict); } else { throw new FormatException($"Unknown KDF UUID: {kdfParams.Uuid}"); } } } } catch (FormatException ex) { throw new KdbxParseException(ReaderResult.FromBadVariantDictionary(ex.Message)); } break; case OuterHeaderField.PublicCustomData: try { VariantDictionary customParams = await VariantDictionary.ReadDictionaryAsync(reader); } catch (FormatException ex) { throw new KdbxParseException(ReaderResult.FromBadVariantDictionary(ex.Message)); } break; } return(fieldId); }
/// <summary> /// Attempts to load KDBX header data from the provided data stream. /// </summary> /// <remarks> /// On success, the HeaderData property of this KdbxReader will be populated. /// On failure, it will be null. /// </remarks> /// <param name="stream">A stream representing a KDBX document (or header).</param> /// <param name="token">A token allowing the operation to be cancelled.</param> /// <returns>A Task representing the result of the read operation.</returns> public async Task <ReaderResult> ReadHeaderAsync(IRandomAccessStream stream, CancellationToken token) { HeaderData = null; using (DataReader reader = GetReaderForStream(stream)) { ReaderResult result = await ValidateSignature(reader); if (result != ReaderResult.Success) { reader.DetachStream(); return(result); } result = await ValidateVersion(reader); if (result != ReaderResult.Success) { reader.DetachStream(); return(result); } // If we get this far, we've passed basic sanity checks. // Construct a HeaderData entity and start reading fields. KdbxHeaderData headerData = new KdbxHeaderData(KdbxHeaderData.Mode.Read); bool gotEndOfHeader = false; while (!gotEndOfHeader) { if (token.IsCancellationRequested) { return(new ReaderResult(KdbxParserCode.OperationCancelled)); } try { OuterHeaderField field = await ReadOuterHeaderField(reader, headerData); if (field == OuterHeaderField.EndOfHeader) { gotEndOfHeader = true; } this.headerInitializationMap[field] = true; } catch (KdbxParseException e) { reader.DetachStream(); return(e.Error); } } // Ensure all headers are initialized bool gotAllHeaders = this.headerInitializationMap.All( kvp => this.headerInitializationMap[kvp.Key] ); if (!gotAllHeaders) { reader.DetachStream(); return(new ReaderResult(KdbxParserCode.HeaderMissing)); } // Hash entire header var sha256 = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256); CryptographicHash hash = sha256.CreateHash(); ulong streamPos = stream.Position; stream.Seek(0); await reader.LoadAsync((uint)streamPos); headerData.FullHeader = reader.ReadBuffer((uint)streamPos); hash.Append(headerData.FullHeader); IBuffer plainHeaderHash = hash.GetValueAndReset(); if (this.parameters.UseXmlHeaderAuthentication) { // The header was parsed successfully - finish creating the object and return success headerData.HeaderHash = CryptographicBuffer.EncodeToBase64String(plainHeaderHash); headerData.Size = streamPos; } if (this.parameters.UseInlineHeaderAuthentication) { // In KDBX4, the header hash is written directly after the header fields. // After the unencrypted hash, an HMAC-SHA-256 hash of the header is written. await reader.LoadAsync(32); IBuffer existingPlainHash = reader.ReadBuffer(32); // Validate plaintext hash if (plainHeaderHash.Length != existingPlainHash.Length) { return(new ReaderResult(KdbxParserCode.BadHeaderHash)); } for (uint i = 0; i < plainHeaderHash.Length; i++) { if (plainHeaderHash.GetByte(i) != existingPlainHash.GetByte(i)) { return(new ReaderResult(KdbxParserCode.BadHeaderHash)); } } DebugHelper.Trace("Validated plaintext KDBX4 header hash"); } if (this.parameters.Version <= KdbxVersion.Three && headerData.KdfParameters == null) { // Need to manually populate KDF parameters for older versions headerData.KdfParameters = new AesParameters( headerData.TransformRounds, headerData.TransformSeed ); } HeaderData = headerData; reader.DetachStream(); return(ReaderResult.Success); } }
/// <summary> /// Attempts to read and validate the next header field in the data stream. /// </summary> /// <param name="reader">A reader over the document file.</param> /// <param name="headerData">The header data that has been extracted so far.</param> /// <returns>A Task representing the field that was read.</returns> private async Task <InnerHeaderField> ReadInnerHeaderField(DataReader reader, KdbxHeaderData headerData) { DebugHelper.Assert(this.parameters.UseInnerHeader); // A header is guaranteed to have 5 bytes at the beginning. // The first byte represents the type or ID of the header. // The next four bytes represent a 32-bit signed integer giving the size of the data field. await reader.LoadAsync(5U); // Read the header ID from the first byte InnerHeaderField fieldId = (InnerHeaderField)reader.ReadByte(); // The cast above may have succeeded but still resulted in an unknown value (outside of the enum). // If so, we need to bail. if (!Enum.IsDefined(typeof(InnerHeaderField), fieldId)) { throw new KdbxParseException(ReaderResult.FromHeaderFieldUnknown((byte)fieldId)); } // Read the header data field size from the next two bytes uint size = reader.ReadUInt32(); DebugHelper.Assert(size <= Int32.MaxValue, "Size should be an Int32"); LoggingFields headerTraceFields = new LoggingFields(); headerTraceFields.AddString("FieldId", fieldId.ToString()); headerTraceFields.AddUInt32("Bytes", size); this.logger.LogEvent("KdbxReader.InnerHeaderField", headerTraceFields, EventVerbosity.Info); await reader.LoadAsync(size); byte[] data = new byte[size]; reader.ReadBytes(data); // Based on the header field in question, the data is validated differently. // The size of the data field needs to be validated, and the data itself may need to be parsed. switch (fieldId) { case InnerHeaderField.EndOfHeader: break; case InnerHeaderField.InnerRandomStreamKey: RequireFieldDataSize(fieldId, size, (n) => n > 0, "must be nonzero"); headerData.InnerRandomStreamKey = data; break; case InnerHeaderField.InnerRandomStreamID: RequireFieldDataSizeEq(fieldId, 4, size); headerData.InnerRandomStream = (RngAlgorithm)BitConverter.ToUInt32(data, 0); RequireEnumDefined(fieldId, headerData.InnerRandomStream); break; case InnerHeaderField.Binary: // Data := F || M where F is one byte and M is the binary content KdbxBinaryFlags flags = (KdbxBinaryFlags)data[0]; ProtectedBinary bin = new ProtectedBinary( data, 1, data.Length - 1, flags.HasFlag(KdbxBinaryFlags.MemoryProtected) ); headerData.ProtectedBinaries.Add(bin); break; } return(fieldId); }