diff --git a/app/src/main/java/eu/depau/etchdroid/utils/blockdevice/BlockDeviceInputStream.kt b/app/src/main/java/eu/depau/etchdroid/utils/blockdevice/BlockDeviceInputStream.kt
new file mode 100644
index 0000000..a980cd3
--- /dev/null
+++ b/app/src/main/java/eu/depau/etchdroid/utils/blockdevice/BlockDeviceInputStream.kt
@@ -0,0 +1,116 @@
+package eu.depau.etchdroid.utils.blockdevice
+
+import com.github.mjdev.libaums.driver.BlockDeviceDriver
+import java.io.InputStream
+import java.nio.ByteBuffer
+
+class BlockDeviceInputStream(
+        private val blockDev: BlockDeviceDriver,
+        private val prefetchBlocks: Int = 2048
+) : InputStream() {
+
+    private val byteBuffer = ByteBuffer.allocate(blockDev.blockSize * prefetchBlocks)
+
+    private var currentBlockOffset: Long = 0
+    private val currentByteOffset: Long
+        get() = currentBlockOffset * blockDev.blockSize + byteBuffer.position()
+
+    private var markedBlock: Long = 0
+    private var markedBufferPosition: Int = 0
+
+    private val sizeBytes: Long
+        get() = blockDev.size.toLong() * blockDev.blockSize
+
+    private fun isNextByteAfterEOF(): Boolean {
+        if (byteBuffer.hasRemaining())
+            return false
+        return currentBlockOffset + 1 >= blockDev.size
+    }
+
+    private fun fetch() {
+        byteBuffer.clear()
+
+        if (blockDev.size - currentBlockOffset < prefetchBlocks)
+            byteBuffer.limit(
+                    (currentBlockOffset - blockDev.size).toInt() * blockDev.blockSize
+            )
+
+        blockDev.read(currentBlockOffset, byteBuffer)
+    }
+
+    private fun fetchNextIfNeeded() {
+        if (byteBuffer.hasRemaining())
+            return
+        currentBlockOffset++
+        fetch()
+    }
+
+    override fun read(): Int {
+        if (isNextByteAfterEOF())
+            return -1
+        fetchNextIfNeeded()
+        return byteBuffer.get().toInt() and 0xFF
+    }
+
+    override fun read(b: ByteArray): Int {
+        return read(b, 0, b.size)
+    }
+
+    override fun read(b: ByteArray, off: Int, len: Int): Int {
+        if (isNextByteAfterEOF())
+            return -1
+
+        val maxPos = (off + len) % (b.size + 1)
+
+        if (len <= 0 || off > b.size)
+            return 0
+
+        var bytesRead = 0
+
+        for (i in off until maxPos) {
+            val readByte = read()
+
+            if (readByte == -1)
+                break
+
+            b[i] = readByte.toByte()
+        }
+
+        return bytesRead
+    }
+
+    override fun skip(n: Long): Long {
+        val actualSkipDistance = when {
+            currentByteOffset + n > sizeBytes -> sizeBytes - currentByteOffset
+            currentByteOffset + n < 0         -> -currentByteOffset
+            else                              -> n
+        }
+
+        val newByteOffset = currentByteOffset + actualSkipDistance
+        currentBlockOffset = newByteOffset / blockDev.blockSize
+
+        fetch()
+        byteBuffer.position((newByteOffset - currentBlockOffset * blockDev.blockSize).toInt())
+
+        return actualSkipDistance
+    }
+
+    override fun available(): Int {
+        return byteBuffer.remaining()
+    }
+
+    override fun mark(readlimit: Int) {
+        markedBlock = currentBlockOffset
+        markedBufferPosition = byteBuffer.position()
+    }
+
+    override fun markSupported(): Boolean {
+        return true
+    }
+
+    override fun reset() {
+        currentBlockOffset = markedBlock
+        fetch()
+        byteBuffer.position(markedBufferPosition)
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/depau/etchdroid/utils/blockdevice/BlockDeviceOutputStream.kt b/app/src/main/java/eu/depau/etchdroid/utils/blockdevice/BlockDeviceOutputStream.kt
new file mode 100644
index 0000000..f9f3a69
--- /dev/null
+++ b/app/src/main/java/eu/depau/etchdroid/utils/blockdevice/BlockDeviceOutputStream.kt
@@ -0,0 +1,85 @@
+package eu.depau.etchdroid.utils.blockdevice
+
+import com.github.mjdev.libaums.driver.BlockDeviceDriver
+import java.io.IOException
+import java.io.OutputStream
+import java.nio.ByteBuffer
+
+class BlockDeviceOutputStream(
+        private val blockDev: BlockDeviceDriver,
+        bufferBlocks: Int = 2048
+) : OutputStream() {
+
+    private val byteBuffer = ByteBuffer.allocate(blockDev.blockSize * bufferBlocks)
+
+    private var currentBlockOffset: Long = 0
+    private val currentByteOffset: Long
+        get() = currentBlockOffset * blockDev.blockSize + byteBuffer.position()
+
+    private val sizeBytes: Long
+        get() = blockDev.size.toLong() * blockDev.blockSize
+
+    private val bytesUntilEOF: Long
+        get() = blockDev.size.toLong() * blockDev.size - currentByteOffset
+
+    override fun write(b: Int) {
+        if (bytesUntilEOF < 1)
+            throw IOException("No space left on device")
+
+        byteBuffer.put(b.toByte())
+
+        if (byteBuffer.remaining() == 0)
+            flush()
+    }
+
+    override fun write(b: ByteArray) {
+        write(b, 0, b.size)
+    }
+
+    override fun write(b: ByteArray, off: Int, len: Int) {
+        val maxPos = (off + len) % (b.size + 1)
+
+        if (len <= 0 || off > b.size)
+            return
+
+        for (i in off until maxPos)
+            write(b[i].toInt())
+    }
+
+    override fun flush() {
+        byteBuffer.flip()
+
+        val toWrite = byteBuffer.limit()
+        val incompleteBlockFullBytes = toWrite % blockDev.blockSize
+        val fullBlocks = (toWrite - incompleteBlockFullBytes) / blockDev.blockSize
+
+        // Check if we're trying to flush while the last written block isn't full
+        if (incompleteBlockFullBytes > 0) {
+            val incompleteBlockBuffer = ByteBuffer.allocate(blockDev.blockSize)
+
+            // Load last block from device
+            blockDev.read(currentBlockOffset + fullBlocks, incompleteBlockBuffer)
+
+            // Add it to the incomplete block
+            byteBuffer.limit(fullBlocks * blockDev.blockSize)
+            byteBuffer.put(
+                    incompleteBlockBuffer.array(),
+                    toWrite, blockDev.blockSize - incompleteBlockFullBytes
+            )
+            byteBuffer.position(0)
+        }
+
+        // Flush to device
+        blockDev.write(currentBlockOffset, byteBuffer)
+
+        // Copy the incomplete block at the beginning, then push back the position
+        byteBuffer.apply {
+            position(fullBlocks * blockDev.blockSize)
+            limit(toWrite)
+            compact()
+            clear()
+            position(incompleteBlockFullBytes)
+        }
+        currentBlockOffset += fullBlocks;
+    }
+}
\ No newline at end of file