diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataFile.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataFile.java index 0b5918ac..92e59092 100644 --- a/src/main/java/org/qortal/arbitrary/ArbitraryDataFile.java +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataFile.java @@ -225,6 +225,21 @@ public class ArbitraryDataFile { return ValidationResult.OK; } + public void validateFileSize(long expectedSize) throws DataException { + // Verify that we can determine the file's size + long fileSize = 0; + try { + fileSize = Files.size(this.getFilePath()); + } catch (IOException e) { + throw new DataException(String.format("Couldn't get file size for transaction %s", Base58.encode(signature))); + } + + // Ensure the file's size matches the size reported by the transaction + if (fileSize != expectedSize) { + throw new DataException(String.format("File size mismatch for transaction %s", Base58.encode(signature))); + } + } + private void addChunk(ArbitraryDataFileChunk chunk) { this.chunks.add(chunk); } diff --git a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java index 568ab5fc..b45390fe 100644 --- a/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java +++ b/src/main/java/org/qortal/arbitrary/ArbitraryDataReader.java @@ -357,6 +357,9 @@ public class ArbitraryDataReader { if (!Arrays.equals(arbitraryDataFile.digest(), digest)) { throw new DataException("Unable to validate complete file hash"); } + // Ensure the file's size matches the size reported by the transaction (throws a DataException if not) + arbitraryDataFile.validateFileSize(transactionData.getSize()); + // Set filePath to the location of the ArbitraryDataFile this.filePath = arbitraryDataFile.getFilePath(); } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java index 5624a57f..0ecb14c5 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java @@ -1,5 +1,7 @@ package org.qortal.repository.hsqldb; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.qortal.arbitrary.misc.Service; import org.qortal.data.arbitrary.ArbitraryResourceInfo; import org.qortal.crypto.Crypto; @@ -13,6 +15,7 @@ import org.qortal.repository.ArbitraryRepository; import org.qortal.repository.DataException; import org.qortal.arbitrary.ArbitraryDataFile; import org.qortal.transaction.Transaction.ApprovalStatus; +import org.qortal.utils.Base58; import java.sql.ResultSet; import java.sql.SQLException; @@ -21,6 +24,8 @@ import java.util.List; public class HSQLDBArbitraryRepository implements ArbitraryRepository { + private static final Logger LOGGER = LogManager.getLogger(HSQLDBArbitraryRepository.class); + private static final int MAX_RAW_DATA_SIZE = 255; // size of VARBINARY protected HSQLDBRepository repository; @@ -66,38 +71,53 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository { } @Override - public byte[] fetchData(byte[] signature) throws DataException { - ArbitraryTransactionData transactionData = getTransactionData(signature); - if (transactionData == null) { - return null; - } + public byte[] fetchData(byte[] signature) { + try { + ArbitraryTransactionData transactionData = getTransactionData(signature); + if (transactionData == null) { + return null; + } - // Raw data is always available - if (transactionData.getDataType() == DataType.RAW_DATA) { - return transactionData.getData(); - } + // Raw data is always available + if (transactionData.getDataType() == DataType.RAW_DATA) { + return transactionData.getData(); + } - // Load hashes - byte[] digest = transactionData.getData(); - byte[] metadataHash = transactionData.getMetadataHash(); + // Load hashes + byte[] digest = transactionData.getData(); + byte[] metadataHash = transactionData.getMetadataHash(); - // Load data file(s) - ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature); - arbitraryDataFile.setMetadataHash(metadataHash); + // Load data file(s) + ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature); + arbitraryDataFile.setMetadataHash(metadataHash); - // If we have the complete data file, return it - if (arbitraryDataFile.exists()) { - return arbitraryDataFile.getBytes(); - } + // If we have the complete data file, return it + if (arbitraryDataFile.exists()) { + // Ensure the file's size matches the size reported by the transaction (throws a DataException if not) + arbitraryDataFile.validateFileSize(transactionData.getSize()); - // Alternatively, if we have all the chunks, combine them into a single file - if (arbitraryDataFile.allChunksExist()) { - arbitraryDataFile.join(); - - // Verify that the combined hash matches the expected hash - if (digest.equals(arbitraryDataFile.digest())) { return arbitraryDataFile.getBytes(); } + + // Alternatively, if we have all the chunks, combine them into a single file + if (arbitraryDataFile.allChunksExist()) { + arbitraryDataFile.join(); + + // Verify that the combined hash matches the expected hash + if (!digest.equals(arbitraryDataFile.digest())) { + LOGGER.info(String.format("Hash mismatch for transaction: %s", Base58.encode(signature))); + return null; + } + + // Ensure the file's size matches the size reported by the transaction + arbitraryDataFile.validateFileSize(transactionData.getSize()); + + return arbitraryDataFile.getBytes(); + } + + } catch (DataException e) { + LOGGER.info("Unable to fetch data for transaction {}: {}", Base58.encode(signature), e.getMessage()); + return null; } return null;