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