You can indeed avoid having an explicit IV for each chunk in favor of an implicit one, and using the previous encryption's last ciphertext block is indeed one way. But:
- You almost certainly need authenticated encryption that provides not just confidentiality but also message authenticity. Read up on the EFail attack, one of whose causes is CBC-based encryption software that doesn't protect ciphertexts from tampering (or more precisely, does so in an inadequate fashion).
- Once you add the message authenticity requirement, it becomes very hard to escape ciphertext expansion (ciphertexts longer than the plaintext) because the ordinary way to achieve it requires you to include a message authenticity tag for each chunk. You can tune the overhead by upping the size of each encrypted chunk, but that of course increases how much memory you need to encrypt/decrypt one.
Be aware that what you're trying to do carries countless pitfalls and that it's much less risky if you use a library written by cryptography experts or, at the very very least, copy one of their designs (and copy it successfully). For example, recent versions of the popular Libsodium library include an encrypted streams and file encryption module that's designed to safely perform precisely this sort of constant memory chunked authenticated encryption, but do it carefully:
This high-level API encrypts a sequence of messages, or a single message split into an arbitrary number of chunks, using a secret key, with the following properties:
- Messages cannot be truncated, removed, reordered, duplicated or modified without this being detected by the decryption functions.
- The same sequence encrypted twice will produce different ciphertexts.
- An authentication tag is added to each encrypted message: stream corruption will be detected early, without having to read the stream until the end.
- Each message can include additional data (ex: timestamp, protocol version) in the computation of the authentication tag.
- Messages can have different sizes.
- There are no practical limits to the total length of the stream, or to the total number of individual messages.
- Ratcheting: at any point in the stream, it is possible to "forget" the key used to encrypt the previous messages, and switch to a new key.
This API can be used to securely send an ordered sequence of messages to a peer. Since the length of the stream is not limited, it can also be used to encrypt files regardless of their size.
It transparently generates nonces and automatically handles key rotation.
Your CBC-based approach won't achieve all of this.
That said, just for theory's sake, an alternative way of avoiding ciphertext expansion in CBC is to use a second key and ECB encryption to generate implicit IVs from a counter or nonce. First you generate and share or derive two block cipher keys $k_1$ (for IV generation) and $k_2$ (for encryption), and a public nonce $\mathrm{IV}$. Then for each numbered chunk $m_1, \dots, m_n$, you encrypt each block this way:
$$
\begin{align}
\mathrm{IV}_1 & = \mathrm{AES}_{k_1}(\mathrm{IV} \oplus 1) \\
c_1 & = \mathrm{AES\text-CBC}_{k_2}^{\mathrm{IV}_1}(m_1) \\
& \vdots \\
\mathrm{IV}_i & = \mathrm{AES}_{k_1}(\mathrm{IV} \oplus i) \\
c_i & = \mathrm{AES\text-CBC}_{k_2}^{\mathrm{IV}_i}(m_i) \\
& \vdots \\
\mathrm{IV}_n & = \mathrm{AES}_{k_1}(\mathrm{IV} \oplus n) \\
c_n & = \mathrm{AES\text-CBC}_{k_2}^{\mathrm{IV}_n}(m_n)
\end{align}
$$
Since the recipient knows $k_1$, $k_2$ and $\mathrm{IV}$, and can also count, they can reconstruct the $\mathrm{IV}_i$ values without the sender having to transmit them.