/// <summary> /// Validates that the document has a valid, supported KeePass version. /// </summary> /// <param name="reader">A DataReader over the document file.</param> /// <returns>A Task representing the result of the validation.</returns> private async Task <ReaderResult> ValidateVersion(DataReader reader) { await reader.LoadAsync(4); uint version = reader.ReadUInt32(); uint maskedVersion = version & FileVersionMask; uint maskedLegacyFormat = FileVersion32_3 & FileVersionMask; uint maskedModernFormat = FileVersion32_4 & FileVersionMask; if (maskedVersion <= maskedLegacyFormat) { this.parameters = new KdbxSerializationParameters(KdbxVersion.Three); } else if (maskedVersion == maskedModernFormat) { this.parameters = new KdbxSerializationParameters(KdbxVersion.Four); } else { DebugHelper.Assert(maskedVersion > maskedModernFormat); return(new ReaderResult(KdbxParserCode.Version)); } // Based on the version, initialize a map of required headers foreach (OuterHeaderField value in Enum.GetValues(typeof(OuterHeaderField)) .Cast <OuterHeaderField>()) { MemberInfo enumMember = typeof(OuterHeaderField).GetMember(value.ToString()) .FirstOrDefault(); // Skip optional headers regardless of version if (enumMember.GetCustomAttribute <OptionalAttribute>() != null) { continue; } // Get the headers that support this version KdbxVersionSupportAttribute versionAttr = enumMember.GetCustomAttribute <KdbxVersionSupportAttribute>(); if (versionAttr == null || versionAttr.Supports(this.parameters.Version)) { this.headerInitializationMap[value] = false; } } return(ReaderResult.Success); }
/// <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); }