|
|
@@ -1,11 +1,13 @@
|
|
|
package com.example.lockx.crypto
|
|
|
|
|
|
import java.io.File
|
|
|
+import java.io.RandomAccessFile
|
|
|
import java.util.zip.CRC32
|
|
|
import javax.crypto.Mac
|
|
|
import javax.crypto.spec.SecretKeySpec
|
|
|
|
|
|
object CryptoUtils {
|
|
|
+
|
|
|
private const val MAGIC = "FMLKv1!!"
|
|
|
private const val VERSION = 1
|
|
|
private const val SALT_SIZE = 16
|
|
|
@@ -38,16 +40,27 @@ object CryptoUtils {
|
|
|
val dk = org.bouncycastle.crypto.generators.SCrypt.generate(
|
|
|
password.toByteArray(Charsets.UTF_8),
|
|
|
salt,
|
|
|
- 1 shl 14, 8, 1, 64
|
|
|
+ 1 shl 14,
|
|
|
+ 8,
|
|
|
+ 1,
|
|
|
+ 64
|
|
|
)
|
|
|
return dk.sliceArray(0..31) to dk.sliceArray(32..63)
|
|
|
}
|
|
|
|
|
|
fun buildVerifier(checkKey: ByteArray): ByteArray {
|
|
|
- return hmacSha256(checkKey, "FMLK-PASSWORD-CHECK".toByteArray(Charsets.UTF_8)).take(VERIFIER_SIZE).toByteArray()
|
|
|
+ return hmacSha256(checkKey, "FMLK-PASSWORD-CHECK".toByteArray(Charsets.UTF_8))
|
|
|
+ .take(VERIFIER_SIZE)
|
|
|
+ .toByteArray()
|
|
|
}
|
|
|
|
|
|
- fun buildTrailer(chunkSize: Long, originalSize: Long, salt: ByteArray, nonce: ByteArray, verifier: ByteArray): ByteArray {
|
|
|
+ fun buildTrailer(
|
|
|
+ chunkSize: Long,
|
|
|
+ originalSize: Long,
|
|
|
+ salt: ByteArray,
|
|
|
+ nonce: ByteArray,
|
|
|
+ verifier: ByteArray
|
|
|
+ ): ByteArray {
|
|
|
val head = ByteArray(TRAILER_SIZE - 4)
|
|
|
var off = 0
|
|
|
for (b in MAGIC.toByteArray()) head[off++] = b
|
|
|
@@ -58,6 +71,7 @@ object CryptoUtils {
|
|
|
salt.copyInto(head, off); off += 16
|
|
|
nonce.copyInto(head, off); off += 16
|
|
|
verifier.copyInto(head, off); off += 16
|
|
|
+
|
|
|
val crc = crc32(head)
|
|
|
val trailer = ByteArray(TRAILER_SIZE)
|
|
|
head.copyInto(trailer)
|
|
|
@@ -98,7 +112,14 @@ object CryptoUtils {
|
|
|
return TrailerMeta(chunkSize, originalSize, salt, nonce, verifier)
|
|
|
}
|
|
|
|
|
|
- fun encryptFile(file: File, password: String, chunkMb: Int, onProgress: ((Long, Long) -> Unit)? = null): Result<Unit> {
|
|
|
+ // ==================== 原地修改版本 ====================
|
|
|
+
|
|
|
+ fun encryptFile(
|
|
|
+ file: File,
|
|
|
+ password: String,
|
|
|
+ chunkMb: Int,
|
|
|
+ onProgress: ((Long, Long) -> Unit)? = null
|
|
|
+ ): Result<Unit> {
|
|
|
return try {
|
|
|
if (!file.exists() || !file.isFile) return Result.failure(Exception("不是有效文件"))
|
|
|
if (isEncrypted(file)) return Result.failure(Exception("已加密"))
|
|
|
@@ -107,132 +128,155 @@ object CryptoUtils {
|
|
|
val originalSize = file.length()
|
|
|
val lockedPath = lockedName(file.absolutePath)
|
|
|
val lockedFile = File(lockedPath)
|
|
|
+
|
|
|
if (lockedFile.exists() && lockedPath != file.absolutePath) {
|
|
|
return Result.failure(Exception("目标文件已存在"))
|
|
|
}
|
|
|
|
|
|
val chunkSize = chunkMb.toLong() * 1024 * 1024
|
|
|
+ val n = minOf(chunkSize, originalSize)
|
|
|
+
|
|
|
val salt = ByteArray(SALT_SIZE).also { java.security.SecureRandom().nextBytes(it) }
|
|
|
val nonce = ByteArray(NONCE_SIZE).also { java.security.SecureRandom().nextBytes(it) }
|
|
|
val (streamKey, checkKey) = deriveKeys(password, salt)
|
|
|
val verifier = buildVerifier(checkKey)
|
|
|
val trailer = buildTrailer(chunkSize, originalSize, salt, nonce, verifier)
|
|
|
- val n = if (chunkSize < originalSize) chunkSize else originalSize
|
|
|
-
|
|
|
- file.inputStream().use { input ->
|
|
|
- lockedFile.outputStream().use { output ->
|
|
|
- var processed = 0L
|
|
|
- var blockOffset = 0
|
|
|
- val bufSize = if (IO_CHUNK_SIZE < n) IO_CHUNK_SIZE.toInt() else n.toInt()
|
|
|
- val buffer = ByteArray(bufSize)
|
|
|
- while (processed < n) {
|
|
|
- val remaining = n - processed
|
|
|
- val readSize = if (buffer.size < remaining) buffer.size else remaining.toInt()
|
|
|
- val bytesRead = input.read(buffer, 0, readSize)
|
|
|
- if (bytesRead <= 0) break
|
|
|
- val plain = buffer.copyOfRange(0, bytesRead)
|
|
|
- val cipher = xorBytes(plain, streamKey, nonce, blockOffset)
|
|
|
- output.write(cipher)
|
|
|
- processed += bytesRead
|
|
|
- blockOffset += (bytesRead + 31) / 32
|
|
|
- onProgress?.invoke(processed, n)
|
|
|
- }
|
|
|
- // 剩余明文原样拷贝(只加密前 n 字节,其余保持原样)
|
|
|
- if (originalSize > n) {
|
|
|
- input.copyTo(output)
|
|
|
- }
|
|
|
- output.write(trailer)
|
|
|
+
|
|
|
+ RandomAccessFile(file, "rw").use { raf ->
|
|
|
+ var processed = 0L
|
|
|
+ var blockOffset = 0
|
|
|
+ val buffer = ByteArray(minOf(IO_CHUNK_SIZE.toLong(), n).toInt())
|
|
|
+
|
|
|
+ while (processed < n) {
|
|
|
+ val remaining = n - processed
|
|
|
+ val readSize = minOf(buffer.size.toLong(), remaining).toInt()
|
|
|
+
|
|
|
+ raf.seek(processed)
|
|
|
+ val bytesRead = raf.read(buffer, 0, readSize)
|
|
|
+ if (bytesRead <= 0) break
|
|
|
+
|
|
|
+ val plain = buffer.copyOfRange(0, bytesRead)
|
|
|
+ val cipher = xorBytes(plain, streamKey, nonce, blockOffset)
|
|
|
+
|
|
|
+ raf.seek(processed)
|
|
|
+ raf.write(cipher)
|
|
|
+
|
|
|
+ processed += bytesRead
|
|
|
+ blockOffset += (bytesRead + 31) / 32
|
|
|
+ onProgress?.invoke(processed, n)
|
|
|
}
|
|
|
+
|
|
|
+ // 追加 trailer
|
|
|
+ raf.seek(originalSize)
|
|
|
+ raf.write(trailer)
|
|
|
}
|
|
|
- if (lockedPath != file.absolutePath) file.delete()
|
|
|
+
|
|
|
+ // 需要加后缀时再 rename
|
|
|
+ if (lockedPath != file.absolutePath) {
|
|
|
+ if (!file.renameTo(lockedFile)) {
|
|
|
+ file.copyTo(lockedFile, overwrite = true)
|
|
|
+ file.delete()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
Result.success(Unit)
|
|
|
- } catch (e: Exception) { Result.failure(e) }
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Result.failure(e)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- fun decryptFile(file: File, password: String, onProgress: ((Long, Long) -> Unit)? = null): Result<Unit> {
|
|
|
+ fun decryptFile(
|
|
|
+ file: File,
|
|
|
+ password: String,
|
|
|
+ onProgress: ((Long, Long) -> Unit)? = null
|
|
|
+ ): Result<Unit> {
|
|
|
return try {
|
|
|
if (!file.exists() || !file.isFile) return Result.failure(Exception("不是有效文件"))
|
|
|
+
|
|
|
val meta = readTrailer(file) ?: return Result.failure(Exception("未加密"))
|
|
|
- if (meta.originalSize + TRAILER_SIZE != file.length()) return Result.failure(Exception("文件损坏"))
|
|
|
+ if (meta.originalSize + TRAILER_SIZE != file.length()) {
|
|
|
+ return Result.failure(Exception("文件损坏"))
|
|
|
+ }
|
|
|
|
|
|
val unlockedPath = unlockedName(file.absolutePath)
|
|
|
val unlockedFile = File(unlockedPath)
|
|
|
+
|
|
|
if (unlockedFile.exists() && unlockedPath != file.absolutePath) {
|
|
|
return Result.failure(Exception("目标文件已存在"))
|
|
|
}
|
|
|
|
|
|
val (streamKey, checkKey) = deriveKeys(password, meta.salt)
|
|
|
val verifier = buildVerifier(checkKey)
|
|
|
- if (!verifier.contentEquals(meta.verifier)) return Result.failure(Exception("密码错误"))
|
|
|
-
|
|
|
- val n = if (meta.chunkSize < meta.originalSize) meta.chunkSize else meta.originalSize
|
|
|
- val tmpFile = File(unlockedFile.absolutePath + ".tmp")
|
|
|
-
|
|
|
- file.inputStream().use { input ->
|
|
|
- tmpFile.outputStream().use { output ->
|
|
|
- var processed = 0L
|
|
|
- var blockOffset = 0
|
|
|
- val bufSize = if (IO_CHUNK_SIZE < n) IO_CHUNK_SIZE.toInt() else n.toInt()
|
|
|
- val buffer = ByteArray(bufSize)
|
|
|
- while (processed < n) {
|
|
|
- val remaining = n - processed
|
|
|
- val readSize = if (buffer.size < remaining) buffer.size else remaining.toInt()
|
|
|
- val bytesRead = input.read(buffer, 0, readSize)
|
|
|
- if (bytesRead <= 0) break
|
|
|
- val cipher = buffer.copyOfRange(0, bytesRead)
|
|
|
- val plain = xorBytes(cipher, streamKey, meta.nonce, blockOffset)
|
|
|
- output.write(plain)
|
|
|
- processed += bytesRead
|
|
|
- blockOffset += (bytesRead + 31) / 32
|
|
|
- onProgress?.invoke(processed, n)
|
|
|
- }
|
|
|
- // 拷贝明文尾部(从位置 n 到 originalSize),trailer 自然留在流末尾被丢弃
|
|
|
- val plainTailSize = meta.originalSize - meta.chunkSize
|
|
|
- if (plainTailSize > 0L) {
|
|
|
- val tailBuf = ByteArray(4 * 1024 * 1024)
|
|
|
- var toCopy = plainTailSize
|
|
|
- while (toCopy > 0L) {
|
|
|
- val toRead = minOf(toCopy, tailBuf.size.toLong()).toInt()
|
|
|
- val bytesRead = input.read(tailBuf, 0, toRead)
|
|
|
- if (bytesRead <= 0) break
|
|
|
- output.write(tailBuf, 0, bytesRead)
|
|
|
- toCopy -= bytesRead
|
|
|
- }
|
|
|
- }
|
|
|
+ if (!verifier.contentEquals(meta.verifier)) {
|
|
|
+ return Result.failure(Exception("密码错误"))
|
|
|
+ }
|
|
|
+
|
|
|
+ val n = minOf(meta.chunkSize, meta.originalSize)
|
|
|
+
|
|
|
+ RandomAccessFile(file, "rw").use { raf ->
|
|
|
+ var processed = 0L
|
|
|
+ var blockOffset = 0
|
|
|
+ val buffer = ByteArray(minOf(IO_CHUNK_SIZE.toLong(), n).toInt())
|
|
|
+
|
|
|
+ while (processed < n) {
|
|
|
+ val remaining = n - processed
|
|
|
+ val readSize = minOf(buffer.size.toLong(), remaining).toInt()
|
|
|
+
|
|
|
+ raf.seek(processed)
|
|
|
+ val bytesRead = raf.read(buffer, 0, readSize)
|
|
|
+ if (bytesRead <= 0) break
|
|
|
+
|
|
|
+ val cipher = buffer.copyOfRange(0, bytesRead)
|
|
|
+ val plain = xorBytes(cipher, streamKey, meta.nonce, blockOffset)
|
|
|
+
|
|
|
+ raf.seek(processed)
|
|
|
+ raf.write(plain)
|
|
|
+
|
|
|
+ processed += bytesRead
|
|
|
+ blockOffset += (bytesRead + 31) / 32
|
|
|
+ onProgress?.invoke(processed, n)
|
|
|
}
|
|
|
+
|
|
|
+ // 截断掉 trailer
|
|
|
+ raf.setLength(meta.originalSize)
|
|
|
}
|
|
|
|
|
|
+ // 需要去后缀时再 rename
|
|
|
if (unlockedPath != file.absolutePath) {
|
|
|
- unlockedFile.delete()
|
|
|
- file.delete()
|
|
|
- if (!tmpFile.renameTo(unlockedFile)) {
|
|
|
- tmpFile.copyTo(unlockedFile, overwrite = true)
|
|
|
- tmpFile.delete()
|
|
|
- }
|
|
|
- } else {
|
|
|
- file.delete()
|
|
|
- if (!tmpFile.renameTo(file)) {
|
|
|
- tmpFile.copyTo(file, overwrite = true)
|
|
|
- tmpFile.delete()
|
|
|
+ if (!file.renameTo(unlockedFile)) {
|
|
|
+ file.copyTo(unlockedFile, overwrite = true)
|
|
|
+ file.delete()
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
Result.success(Unit)
|
|
|
- } catch (e: Exception) { Result.failure(e) }
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Result.failure(e)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- private fun xorBytes(data: ByteArray, key: ByteArray, nonce: ByteArray, blockOffset: Int): ByteArray {
|
|
|
+ // ==================== 内部工具方法 ====================
|
|
|
+
|
|
|
+ private fun xorBytes(
|
|
|
+ data: ByteArray,
|
|
|
+ key: ByteArray,
|
|
|
+ nonce: ByteArray,
|
|
|
+ blockOffset: Int
|
|
|
+ ): ByteArray {
|
|
|
if (data.isEmpty()) return ByteArray(0)
|
|
|
val out = ByteArray(data.size)
|
|
|
var i = 0
|
|
|
while (i < data.size) {
|
|
|
- val blockSize = if (32 < data.size - i) 32 else data.size - i
|
|
|
+ val blockSize = minOf(32, data.size - i)
|
|
|
val counter = blockOffset + i / 32
|
|
|
val counterBytes = counter.toLong().toBEBytes(8)
|
|
|
val combined = ByteArray(nonce.size + counterBytes.size)
|
|
|
nonce.copyInto(combined, 0)
|
|
|
counterBytes.copyInto(combined, nonce.size)
|
|
|
val ks = hmacSha256(key, combined)
|
|
|
- for (j in 0 until blockSize) out[i + j] = ((data[i + j].toInt() xor ks[j].toInt()) and 0xFF).toByte()
|
|
|
+ for (j in 0 until blockSize) {
|
|
|
+ out[i + j] = ((data[i + j].toInt() xor ks[j].toInt()) and 0xFF).toByte()
|
|
|
+ }
|
|
|
i += blockSize
|
|
|
}
|
|
|
return out
|
|
|
@@ -254,7 +298,6 @@ object CryptoUtils {
|
|
|
return mac.doFinal(data)
|
|
|
}
|
|
|
|
|
|
- /** CRC32 只占 4 字节,用独立函数避免 8 字节越界 */
|
|
|
private fun packBigEndian4(buf: ByteArray, offset: Int, value: Long) {
|
|
|
var v = value
|
|
|
for (i in 3 downTo 0) {
|
|
|
@@ -265,7 +308,9 @@ object CryptoUtils {
|
|
|
|
|
|
private fun unpackBigEndian4(buf: ByteArray, offset: Int): Long {
|
|
|
var result = 0L
|
|
|
- for (i in offset until offset + 4) result = (result shl 8) or (buf[i].toLong() and 0xFFL)
|
|
|
+ for (i in offset until offset + 4) {
|
|
|
+ result = (result shl 8) or (buf[i].toLong() and 0xFFL)
|
|
|
+ }
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
@@ -279,7 +324,9 @@ object CryptoUtils {
|
|
|
|
|
|
private fun unpackBigEndian(buf: ByteArray, offset: Int): Long {
|
|
|
var result = 0L
|
|
|
- for (i in offset until offset + 8) result = (result shl 8) or (buf[i].toLong() and 0xFFL)
|
|
|
+ for (i in offset until offset + 8) {
|
|
|
+ result = (result shl 8) or (buf[i].toLong() and 0xFFL)
|
|
|
+ }
|
|
|
return result
|
|
|
}
|
|
|
|