首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >用ExoPlayer复制加密视频

用ExoPlayer复制加密视频
EN

Stack Overflow用户
提问于 2016-08-02 19:44:54
回答 5查看 6.4K关注 0票数 26

我正在使用ExoPlayer,在安卓系统中,我试图复制一个本地存储的加密视频。

ExoPlayer的模块化允许创建可以注入到ExoPlayer中的自定义组件,情况似乎是这样的。事实上,经过一些研究之后,我意识到为了完成这个任务,我可以创建一个定制的DataSource并覆盖open()read()close()

我也找到了这个解决方案,但实际上在这里,整个文件被一步解密,并存储在一个清晰的输入流中。这在很多情况下都是好的。但如果我需要复制大文件呢?

所以问题是:如何在ExoPlayer中复制加密视频,对内容进行“动态”解密(而不对整个文件进行解密)?这有可能吗?

我尝试创建一个具有open()方法的自定义DataSource:

代码语言:javascript
运行
复制
@Override
    public long open(DataSpec dataSpec) throws FileDataSourceException {
        try {
            File file = new File(dataSpec.uri.getPath());

            clearInputStream = new CipherInputStream(new FileInputStream(file), mCipher);

            long skipped = clearInputStream.skip(dataSpec.position);
            if (skipped < dataSpec.position) {
                throw new EOFException();
            }
            if (dataSpec.length != C.LENGTH_UNBOUNDED) {
                bytesRemaining = dataSpec.length;
            } else {
                bytesRemaining = clearInputStream.available();
                if (bytesRemaining == 0) {
                    bytesRemaining = C.LENGTH_UNBOUNDED;
                }
            }
        } catch (EOFException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        opened = true;
        if (listener != null) {
            listener.onTransferStart();
        }

        return bytesRemaining;
    }

下面是read()方法:

代码语言:javascript
运行
复制
@Override
public int read(byte[] buffer, int offset, int readLength) throws FileDataSourceException {
        if (bytesRemaining == 0) {
            return -1;
        } else {
            int bytesRead = 0;

                int bytesToRead = bytesRemaining == C.LENGTH_UNBOUNDED ? readLength
                        : (int) Math.min(bytesRemaining, readLength);
            try {
                bytesRead = clearInputStream.read(buffer, offset, bytesToRead);
            } catch (IOException e) {
                e.printStackTrace();
            }

            if (bytesRead > 0) {
                if (bytesRemaining != C.LENGTH_UNBOUNDED) {
                    bytesRemaining -= bytesRead;
                }
                if (listener != null) {
                    listener.onBytesTransferred(bytesRead);
                }
            }

            return bytesRead;
        }
    }

如果我传递一个清晰的文件而不是编码的文件,然后删除CipherInputStream部件,那么它可以正常工作,而对于加密的文件,我得到了以下错误:

代码语言:javascript
运行
复制
    Unexpected exception loading stream
java.lang.IllegalStateException: Top bit not zero: -1195853062
at com.google.android.exoplayer.util.ParsableByteArray.readUnsignedIntToInt(ParsableByteArray.java:240)
at com.google.android.exoplayer.extractor.mp4.Mp4Extractor.readSample(Mp4Extractor.java:331)
at com.google.android.exoplayer.extractor.mp4.Mp4Extractor.read(Mp4Extractor.java:122)
at com.google.android.exoplayer.extractor.ExtractorSampleSource$ExtractingLoadable.load(ExtractorSampleSource.java:745)
at com.google.android.exoplayer.upstream.Loader$LoadTask.run(Loader.java:209)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:423)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)

编辑

加密视频是以这种方式生成的:

代码语言:javascript
运行
复制
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec("0123456789012345".getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec("0123459876543210".getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);

outputStream = new CipherOutputStream(output_stream, cipher);

然后将outputStream保存到文件中。

EN

回答 5

Stack Overflow用户

回答已采纳

发布于 2016-08-12 19:45:34

最终我找到了解决办法。

我在加密算法中使用了一种不填充的方法,这样:

代码语言:javascript
运行
复制
cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC");

这样,加密文件的大小和清除的文件大小保持不变。所以现在我创建了流:

代码语言:javascript
运行
复制
cipherInputStream = new CipherInputStream(inputStream, cipher) {
    @Override
    public int available() throws IOException {
         return in.available();
    }
};

这是因为Java文档提到了ChiperInputStream.available()

凌驾

实际上,我认为这更像是必须的,因为从该方法中检索到的值通常是非常奇怪的。

就这样!现在它工作得很好。

票数 4
EN

Stack Overflow用户

发布于 2019-02-15 15:18:31

例如如何播放加密的音频文件,希望这将对某人有所帮助。我在这里用科特林

代码语言:javascript
运行
复制
import android.net.Uri
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DataSourceInputStream
import com.google.android.exoplayer2.upstream.DataSpec
import com.google.android.exoplayer2.util.Assertions
import java.io.IOException
import javax.crypto.CipherInputStream

class EncryptedDataSource(upstream: DataSource) : DataSource {

    private var upstream: DataSource? = upstream
    private var cipherInputStream: CipherInputStream? = null

    override fun open(dataSpec: DataSpec?): Long {
        val cipher = getCipherInitDecrypt()
        val inputStream = DataSourceInputStream(upstream, dataSpec)
        cipherInputStream = CipherInputStream(inputStream, cipher)
        inputStream.open()
        return C.LENGTH_UNSET.toLong()

    }

    override fun read(buffer: ByteArray?, offset: Int, readLength: Int): Int {
        Assertions.checkNotNull<Any>(cipherInputStream)
        val bytesRead = cipherInputStream!!.read(buffer, offset, readLength)
        return if (bytesRead < 0) {
            C.RESULT_END_OF_INPUT
        } else bytesRead
    }

    override fun getUri(): Uri {
        return upstream!!.uri
    }

    @Throws(IOException::class)
    override fun close() {
        if (cipherInputStream != null) {
            cipherInputStream = null
            upstream!!.close()
        }
    }
}

在上面的函数中,您需要获得用于加密和init :smth的密码。

代码语言:javascript
运行
复制
fun getCipherInitDecrypt(): Cipher {
    val cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC");
    val iv = IvParameterSpec(initVector.toByteArray(charset("UTF-8")))
    val skeySpec = SecretKeySpec(key, TYPE_RSA)
    cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv)
    return cipher
}

下一步是为我们前面实现的DataSource.FactoryDataSource创建

代码语言:javascript
运行
复制
import com.google.android.exoplayer2.upstream.DataSource

class EncryptedFileDataSourceFactory(var dataSource: DataSource) : DataSource.Factory {

    override fun createDataSource(): DataSource {
        return EncryptedDataSource(dataSource)
    }
}

最后一步是玩家初始化。

代码语言:javascript
运行
复制
    private fun prepareExoPlayerFromFileUri(uri: Uri) {
        val player = ExoPlayerFactory.newSimpleInstance(
                    DefaultRenderersFactory(this),
                    DefaultTrackSelector(),
                    DefaultLoadControl())

        val playerView = findViewById<PlayerView>(R.id.player_view)
        playerView.player = player

        val dsf = DefaultDataSourceFactory(this, Util.getUserAgent(this, "ExoPlayerInfo"))
        //This line do the thing
        val mediaSource = ExtractorMediaSource.Factory(EncryptedFileDataSourceFactory(dsf.createDataSource())).createMediaSource(uri)
        player.prepare(mediaSource)
    }
票数 5
EN

Stack Overflow用户

发布于 2020-11-12 07:34:21

这个问题让我大发雷霆,所以我终于放弃了,并为AES/CBC实现了一个流密码,允许您跳过前面。从理论上讲,CBC允许随机读取,您需要使用前面块的密文作为初始化向量初始化密码,然后先读取到所需的点。具有完整实现的示例项目( 这里。 )如下是关键类:

代码语言:javascript
运行
复制
import android.net.Uri
import android.util.Log
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DataSpec
import com.google.android.exoplayer2.upstream.TransferListener
import ar.cryptotest.exoplayer2.MainActivity.Companion.AES_TRANSFORMATION
import java.io.EOFException
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.lang.RuntimeException
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec

const val TAG = "ENCRYPTING PROCESS"

class BlockCipherEncryptedDataSource(
    private val secretKeySpec: SecretKeySpec,
    private val uri: Uri,
    cipherTransformation: String = "AES/CBC/PKCS7Padding"
) : DataSource {
    private val cipher: Cipher = Cipher.getInstance(cipherTransformation)
    private lateinit var streamingCipherInputStream: StreamingCipherInputStream
    private var bytesRemaining: Long = 0
    private var isOpen = false
    private val transferListeners = mutableListOf<TransferListener>()
    private var dataSpec: DataSpec? = null

    @Throws(EncryptedFileDataSourceException::class)
    override fun open(dataSpec: DataSpec): Long {
        this.dataSpec = dataSpec

        if (isOpen) return bytesRemaining

        try {
            setupInputStream()
            streamingCipherInputStream.forceSkip(dataSpec.position)
            computeBytesRemaining(dataSpec)
        } catch (e: IOException) {
            throw EncryptedFileDataSourceException(e)
        }

        isOpen = true
        transferListeners.forEach { it.onTransferStart(this, dataSpec, false) }

        return C.LENGTH_UNSET.toLong()
    }

    private fun setupInputStream() {
        val path = uri.path ?: throw RuntimeException("Tried decrypting uri with no path: $uri")
        val encryptedFileStream = File(path).inputStream()
        val initializationVector = ByteArray(cipher.blockSize)
        encryptedFileStream.read(initializationVector)
        streamingCipherInputStream =
            StreamingCipherInputStream(
                encryptedFileStream,
                cipher,
                IvParameterSpec(initializationVector),
                secretKeySpec
            )
    }

    @Throws(IOException::class)
    private fun computeBytesRemaining(dataSpec: DataSpec) {
        if (dataSpec.length != C.LENGTH_UNSET.toLong()) {
            bytesRemaining = dataSpec.length
            return
        }

        if (bytesRemaining == Int.MAX_VALUE.toLong()) {
            bytesRemaining = C.LENGTH_UNSET.toLong()
            return
        }

        bytesRemaining = streamingCipherInputStream.available().toLong()
    }

    @Throws(EncryptedFileDataSourceException::class)
    override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int {
        if (bytesRemaining == 0L) {
            Log.e(TAG, "End - No bytes remaining")
            return C.RESULT_END_OF_INPUT
        }

        val bytesRead = try {
            streamingCipherInputStream.read(buffer, offset, readLength)
        } catch (e: IOException) {
            throw EncryptedFileDataSourceException(e)
        }

        // Reading -1 means an error occurred
        if (bytesRead < 0) {
            if (bytesRemaining != C.LENGTH_UNSET.toLong())
                throw EncryptedFileDataSourceException(EOFException())
            return C.RESULT_END_OF_INPUT
        }

        // Bytes remaining will be unset if file is too large for an int
        if (bytesRemaining != C.LENGTH_UNSET.toLong())
            bytesRemaining -= bytesRead.toLong()

        dataSpec?.let { nonNullDataSpec ->
            transferListeners.forEach {
                it.onBytesTransferred(this, nonNullDataSpec, false, bytesRead)
            }
        }
        return bytesRead
    }

    override fun addTransferListener(transferListener: TransferListener) {
        transferListeners.add(transferListener)
    }

    override fun getUri(): Uri = uri

    @Throws(EncryptedFileDataSourceException::class)
    override fun close() {
        Log.e(TAG, "Closing stream")
        try {
            streamingCipherInputStream.close()
        } catch (e: IOException) {
            throw EncryptedFileDataSourceException(e)
        } finally {
            if (isOpen) {
                isOpen = false
                dataSpec?.let { nonNullDataSpec ->
                    transferListeners.forEach { it.onTransferEnd(this, nonNullDataSpec, false) }
                }
            }
        }
    }

    class EncryptedFileDataSourceException(cause: IOException?) : IOException(cause)
    class StreamingCipherInputStream(
        private val sourceStream: InputStream,
        private var cipher: Cipher,
        private val initialIvParameterSpec: IvParameterSpec,
        private val secretKeySpec: SecretKeySpec
    ) : CipherInputStream(
        sourceStream, cipher
    ) {
        private val cipherBlockSize: Int = cipher.blockSize

        @Throws(IOException::class)
        override fun read(b: ByteArray, off: Int, len: Int): Int = super.read(b, off, len)

        fun forceSkip(bytesToSkip: Long) {
            val bytesSinceStartOfCurrentBlock = bytesToSkip % cipherBlockSize

            val bytesUntilPreviousBlockStart =
                bytesToSkip - bytesSinceStartOfCurrentBlock - cipherBlockSize

            try {
                if (bytesUntilPreviousBlockStart <= 0) {
                    cipher.init(
                        Cipher.DECRYPT_MODE,
                        secretKeySpec,
                        initialIvParameterSpec
                    )
                    return
                }

                var skipped = sourceStream.skip(bytesUntilPreviousBlockStart)
                while (skipped < bytesUntilPreviousBlockStart) {
                    sourceStream.read()
                    skipped++
                }

                val previousEncryptedBlock = ByteArray(cipherBlockSize)

                sourceStream.read(previousEncryptedBlock)

                cipher.init(
                    Cipher.DECRYPT_MODE,
                    secretKeySpec,
                    IvParameterSpec(previousEncryptedBlock)
                )
                skip(bytesUntilPreviousBlockStart + cipherBlockSize)

                val discardableByteArray = ByteArray(bytesSinceStartOfCurrentBlock.toInt())
                read(discardableByteArray)
            } catch (e: Exception) {
                Log.e(TAG, "Encrypted video skipping error", e)
                throw e
            }
        }

        // We need to return the available bytes from the upstream.
        // In this implementation we're front loading it, but it's possible the value might change during the lifetime
        // of this instance, and reference to the stream should be retained and queried for available bytes instead
        @Throws(IOException::class)
        override fun available(): Int {
            return sourceStream.available()
        }
    }
}

class BlockCipherEncryptedDataSourceFactory(
    private val secretKeySpec: SecretKeySpec,
    private val uri: Uri,
    private val cipherTransformation: String = "AES/CBC/PKCS7Padding"
) : DataSource.Factory {
    override fun createDataSource(): BlockCipherEncryptedDataSource {
        return BlockCipherEncryptedDataSource(secretKeySpec, uri, cipherTransformation)
    }
}
票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/38729220

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档