/// <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> /// Writes out the KDBX header. /// </summary> /// <param name="writer">The DataWriter to write to.</param> /// <returns>A Task representing the asynchronous operation.</returns> private async Task WriteOuterHeaderAsync(DataWriter writer) { WriteFieldId(writer, OuterHeaderField.CipherID); WriteFieldSize(writer, 16); writer.WriteBytes(GetCipherUuid(HeaderData.Cipher).ToByteArray()); await writer.StoreAsync(); WriteFieldId(writer, OuterHeaderField.CompressionFlags); WriteFieldSize(writer, 4); writer.WriteUInt32((uint)HeaderData.Compression); await writer.StoreAsync(); WriteFieldId(writer, OuterHeaderField.MasterSeed); WriteFieldSize(writer, 32); writer.WriteBuffer(HeaderData.MasterSeed); await writer.StoreAsync(); if (!this.parameters.UseExtensibleKdf) { AesParameters kdfParams = HeaderData.KdfParameters as AesParameters; DebugHelper.Assert(kdfParams != null); WriteFieldId(writer, OuterHeaderField.TransformSeed); WriteFieldSize(writer, 32); writer.WriteBuffer(kdfParams.Seed); await writer.StoreAsync(); WriteFieldId(writer, OuterHeaderField.TransformRounds); WriteFieldSize(writer, 8); writer.WriteUInt64(kdfParams.Rounds); await writer.StoreAsync(); } else { WriteFieldId(writer, OuterHeaderField.KdfParameters); VariantDictionary kdfDict = HeaderData.KdfParameters.ToVariantDictionary(); WriteFieldSize(writer, (uint)kdfDict.GetSerializedSize()); await kdfDict.WriteToAsync(writer); } WriteFieldId(writer, OuterHeaderField.EncryptionIV); WriteFieldSize(writer, HeaderData.EncryptionIV.Length); writer.WriteBuffer(HeaderData.EncryptionIV); await writer.StoreAsync(); if (!this.parameters.UseHmacBlocks) { WriteFieldId(writer, OuterHeaderField.StreamStartBytes); WriteFieldSize(writer, HeaderData.StreamStartBytes.Length); writer.WriteBuffer(HeaderData.StreamStartBytes); await writer.StoreAsync(); } if (!this.parameters.UseInnerHeader) { WriteFieldId(writer, OuterHeaderField.ProtectedStreamKey); WriteFieldSize(writer, 32); writer.WriteBytes(HeaderData.InnerRandomStreamKey); await writer.StoreAsync(); WriteFieldId(writer, OuterHeaderField.InnerRandomStreamID); WriteFieldSize(writer, 4); writer.WriteUInt32((uint)HeaderData.InnerRandomStream); await writer.StoreAsync(); } WriteFieldId(writer, OuterHeaderField.EndOfHeader); WriteFieldSize(writer, 4); writer.WriteByte(0x0D); writer.WriteByte(0x0A); writer.WriteByte(0x0D); writer.WriteByte(0x0A); await writer.StoreAsync(); }