Ejemplo n.º 1
0
        /// <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);
        }
Ejemplo n.º 2
0
        /// <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
            };
        }
Ejemplo n.º 3
0
        /// <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);
        }
Ejemplo n.º 4
0
        /// <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);
            }
        }
Ejemplo n.º 5
0
        /// <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);
        }