Doc comments for AES-CTR, AES-GCM, and AES-GCM-SIV APIs

This commit is contained in:
Jordan Rose
2025-11-17 12:58:58 -08:00
parent 9b63526808
commit 94f030cdb7
8 changed files with 261 additions and 12 deletions

View File

@@ -11,6 +11,13 @@ import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
/**
* Implements the <a
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)">AES-256-CTR</a>
* stream cipher with a 12-byte nonce and an initial counter.
*
* <p>CTR mode is built on XOR, so encrypting and decrypting are the same operation.
*/
public class Aes256Ctr32 extends NativeHandleGuard.SimpleOwner {
public Aes256Ctr32(byte[] key, byte[] nonce, int initialCtr) throws InvalidKeyException {
super(
@@ -23,10 +30,20 @@ public class Aes256Ctr32 extends NativeHandleGuard.SimpleOwner {
Native.Aes256Ctr32_Destroy(nativeHandle);
}
/**
* Encrypts the plaintext, or decrypts the ciphertext, in {@code data}, in place, advancing the
* state of the cipher.
*/
public void process(byte[] data) {
this.process(data, 0, data.length);
}
/**
* Encrypts the plaintext, or decrypts the ciphertext, in {@code data}, in place, advancing the
* state of the cipher.
*
* <p>Bytes outside the designated offset/length are unchanged.
*/
public void process(byte[] data, int offset, int length) {
guardedRun((nativeHandle) -> Native.Aes256Ctr32_Process(nativeHandle, data, offset, length));
}

View File

@@ -11,9 +11,27 @@ import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
/**
* Implements the <a
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM)">AES-256-GCM</a>
* authenticated stream cipher with a 12-byte nonce.
*
* <p>This API exposes the streaming nature of AES-GCM to allow decrypting data without having it
* resident in memory all at once. You <strong>must</strong> call {@link #verifyTag} when the
* decryption is complete, or else you have no authenticity guarantees.
*
* @see Aes256GcmEncryption
*/
public class Aes256GcmDecryption extends NativeHandleGuard.SimpleOwner {
/** The size of the authentication tag, as used by {@link #verifyTag} */
public static final int TAG_SIZE_IN_BYTES = 16;
/**
* Initializes the cipher with the given inputs.
*
* <p>The associated data is not included in the plaintext or tag; instead, it's expected to match
* between the encrypter and decrypter.
*/
public Aes256GcmDecryption(byte[] key, byte[] nonce, byte[] associatedData)
throws InvalidKeyException {
super(
@@ -27,18 +45,35 @@ public class Aes256GcmDecryption extends NativeHandleGuard.SimpleOwner {
Native.Aes256GcmDecryption_Destroy(nativeHandle);
}
public void decrypt(byte[] plaintext) {
/**
* Decrypts {@code ciphertext} in place and advances the state of the cipher.
*
* <p>Don't forget to call {@link #verifyTag} when decryption is complete.
*/
public void decrypt(byte[] ciphertext) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmDecryption_Update(nativeHandle, plaintext, 0, plaintext.length));
Native.Aes256GcmDecryption_Update(nativeHandle, ciphertext, 0, ciphertext.length));
}
public void decrypt(byte[] plaintext, int offset, int length) {
/**
* Decrypts {@code ciphertext} in place and advances the state of the cipher.
*
* <p>Bytes outside the designated offset/length are unchanged.
*
* <p>Don't forget to call {@link #verifyTag} when decryption is complete.
*/
public void decrypt(byte[] ciphertext, int offset, int length) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmDecryption_Update(nativeHandle, plaintext, offset, length));
Native.Aes256GcmDecryption_Update(nativeHandle, ciphertext, offset, length));
}
/**
* Returns {@code true} if and only if {@code tag} matches the ciphertext that has been processed.
*
* <p>After calling {@code verifyTag}, this object may not be used anymore.
*/
public boolean verifyTag(byte[] tag) {
return guardedMap(
(nativeHandle) ->

View File

@@ -11,7 +11,25 @@ import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
/**
* Implements the <a
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM)">AES-256-GCM</a>
* authenticated stream cipher with a 12-byte nonce.
*
* <p>This API exposes the streaming nature of AES-GCM to allow encrypting data without having it
* resident in memory all at once. You must call {@link #computeTag} when the encryption is complete
* to produce the authentication tag for the ciphertext, and then make sure the tag makes it to the
* decrypter.
*
* @see Aes256GcmDecryption
*/
public class Aes256GcmEncryption extends NativeHandleGuard.SimpleOwner {
/**
* Initializes the cipher with the given inputs.
*
* <p>The associated data is not included in the plaintext or tag; instead, it's expected to match
* between the encrypter and decrypter. If you don't need any extra data, pass an empty array.
*/
public Aes256GcmEncryption(byte[] key, byte[] nonce, byte[] associatedData)
throws InvalidKeyException {
super(
@@ -25,18 +43,35 @@ public class Aes256GcmEncryption extends NativeHandleGuard.SimpleOwner {
Native.Aes256GcmEncryption_Destroy(nativeHandle);
}
/**
* Encrypts {@code plaintext} in place and advances the state of the cipher.
*
* <p>Bytes outside the designated offset/length are unchanged.
*
* <p>Don't forget to call {@link #computeTag} when encryption is complete.
*/
public void encrypt(byte[] plaintext, int offset, int length) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmEncryption_Update(nativeHandle, plaintext, offset, length));
}
/**
* Encrypts {@code plaintext} in place and advances the state of the cipher.
*
* <p>Don't forget to call {@link #computeTag} when encryption is complete.
*/
public void encrypt(byte[] plaintext) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmEncryption_Update(nativeHandle, plaintext, 0, plaintext.length));
}
/**
* Produces an authentication tag for the plaintext that has been processed.
*
* <p>After calling {@code computeTag}, this object may not be used anymore.
*/
public byte[] computeTag() {
return guardedMap(Native::Aes256GcmEncryption_ComputeTag);
}

View File

@@ -12,6 +12,13 @@ import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidMessageException;
/**
* Implements the <a href="https://en.wikipedia.org/wiki/AES-GCM-SIV">AES-256-GCM-SIV</a>
* authenticated stream cipher with a 12-byte nonce.
*
* <p>AES-GCM-SIV is a multi-pass algorithm (to generate the "synthetic initialization vector"), so
* this API does not expose a streaming form.
*/
public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
public Aes256GcmSiv(byte[] key) throws InvalidKeyException {
@@ -23,6 +30,15 @@ public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
Native.Aes256GcmSiv_Destroy(nativeHandle);
}
/**
* Encrypts the given plaintext using the given nonce, and authenticating the ciphertext and given
* associated data.
*
* <p>The associated data is not included in the ciphertext; instead, it's expected to match
* between the encrypter and decrypter. If you don't need any extra data, pass an empty array.
*
* @return The encrypted data, including an appended 16-byte authentication tag.
*/
public byte[] encrypt(byte[] plaintext, byte[] nonce, byte[] associated_data) {
return filterExceptions(
() ->
@@ -31,6 +47,15 @@ public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
Native.Aes256GcmSiv_Encrypt(nativeHandle, plaintext, nonce, associated_data)));
}
/**
* Decrypts the given ciphertext using the given nonce, and authenticating the ciphertext and
* given associated data.
*
* <p>The associated data is not included in the ciphertext; instead, it's expected to match
* between the encrypter and decrypter.
*
* @return The decrypted data
*/
public byte[] decrypt(byte[] ciphertext, byte[] nonce, byte[] associated_data)
throws InvalidMessageException {
return filterExceptions(

View File

@@ -136,6 +136,13 @@ export class Fingerprint {
}
}
/**
* Implements the <a href="https://en.wikipedia.org/wiki/AES-GCM-SIV">AES-256-GCM-SIV</a>
* authenticated stream cipher with a 12-byte nonce.
*
* AES-GCM-SIV is a multi-pass algorithm (to generate the "synthetic initialization vector"), so
* this API does not expose a streaming form.
*/
export class Aes256GcmSiv {
readonly _nativeHandle: Native.Aes256GcmSiv;
@@ -147,20 +154,38 @@ export class Aes256GcmSiv {
return new Aes256GcmSiv(key);
}
/**
* Encrypts the given plaintext using the given nonce, and authenticating the ciphertext and given
* associated data.
*
* The associated data is not included in the ciphertext; instead, it's expected to match between
* the encrypter and decrypter. If you don't need any extra data, pass an empty array.
*
* @returns The encrypted data, including an appended 16-byte authentication tag.
*/
encrypt(
message: Uint8Array,
nonce: Uint8Array,
associated_data: Uint8Array
associatedData: Uint8Array
): Uint8Array {
return Native.Aes256GcmSiv_Encrypt(this, message, nonce, associated_data);
return Native.Aes256GcmSiv_Encrypt(this, message, nonce, associatedData);
}
/**
* Decrypts the given ciphertext using the given nonce, and authenticating the ciphertext and given
* associated data.
*
* The associated data is not included in the ciphertext; instead, it's expected to match between
* the encrypter and decrypter.
*
* @returns The decrypted data
*/
decrypt(
message: Uint8Array,
nonce: Uint8Array,
associated_data: Uint8Array
associatedData: Uint8Array
): Uint8Array {
return Native.Aes256GcmSiv_Decrypt(this, message, nonce, associated_data);
return Native.Aes256GcmSiv_Decrypt(this, message, nonce, associatedData);
}
}

View File

@@ -6,10 +6,19 @@
import Foundation
import SignalFfi
/// Implements the
/// [AES-256-CTR](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR))
/// stream cipher with a combined 12-byte nonce and initial 4-byte counter.
///
/// CTR mode is built on XOR, so encrypting and decrypting are the same operation.
public class Aes256Ctr32: NativeHandleOwner<SignalMutPointerAes256Ctr32> {
public static let keyLength: Int = 32
public static let nonceLength: Int = 16
/// Initializes the cipher with the given key and combined nonce.
///
/// The first 12 bytes of the nonce are treated as a traditional nonce, while the last four are
/// treated as the initial counter---the position in the cipher stream to start at.
public convenience init<KeyBytes, NonceBytes>(
key: KeyBytes,
nonce: NonceBytes
@@ -49,6 +58,8 @@ public class Aes256Ctr32: NativeHandleOwner<SignalMutPointerAes256Ctr32> {
return signal_aes256_ctr32_destroy(handle.pointer)
}
/// Encrypts the plaintext, or decrypts the ciphertext, in `message`, in place, advancing the
/// state of the cipher.
public func process(_ message: inout Data) throws {
try withNativeHandle { nativeHandle in
try message.withUnsafeMutableBytes { messageBytes in
@@ -64,6 +75,10 @@ public class Aes256Ctr32: NativeHandleOwner<SignalMutPointerAes256Ctr32> {
}
}
/// Encrypts the plaintext, or decrypts the ciphertext, in `message`, in place, using the given
/// key and nonce.
///
/// This is a convenience for when the entire message fits in memory.
public static func process<KeyBytes, NonceBytes>(
_ message: inout Data,
key: KeyBytes,

View File

@@ -6,6 +6,13 @@
import Foundation
import SignalFfi
/// Provides convenient use of the
/// [AES-256-GCM](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM))
/// authenticated stream cipher with a 12-byte nonce.
///
/// This struct packs up all the data needed to transmit an AES-GCM ciphertext, and makes it easy to
/// operate on a full ciphertext at once. If you need streaming encryption or decryption, you can
/// use the more manual APIs ``Aes256GcmEncryption`` and ``Aes256GcmDecryption``.
public struct Aes256GcmEncryptedData: Sendable {
public static let keyLength: Int = 32
public static let nonceLength: Int = 12
@@ -22,6 +29,11 @@ public struct Aes256GcmEncryptedData: Sendable {
self.authenticationTag = authenticationTag
}
/// Assumes `concatenated` is the concatenation `nonce || ciphertext || authenticationTag`, and
/// splits it up accordingly.
///
/// Throws if the data is too short to contain all three parts (though technically the
/// ciphertext may be empty).
@inlinable
public init(concatenated: Data) throws {
guard concatenated.count >= Self.nonceLength + Self.authenticationTagLength else {
@@ -32,6 +44,10 @@ public struct Aes256GcmEncryptedData: Sendable {
self.authenticationTag = concatenated.suffix(Self.authenticationTagLength)
}
/// Concatenates the nonce, ciphertext, and authentication tag, in that order.
///
/// This is a fairly standard way to send AES-GCM ciphertexts. The result is suitable for
/// passing to ``init(concatenated:)``.
public func concatenate() -> Data {
var result = Data(capacity: nonce.count + self.ciphertext.count + self.authenticationTag.count)
result += self.nonce
@@ -40,6 +56,14 @@ public struct Aes256GcmEncryptedData: Sendable {
return result
}
/// Encrypts the given plaintext using the given key, and authenticating the ciphertext and
/// given associated data.
///
/// The associated data is not included in the ciphertext; instead, it's expected to match
/// between the encrypter and decrypter. If you don't need any extra data, use the other
/// overload ``encrypt(_:key:)``.
///
/// This API will generate a random nonce, which is included in the result.
public static func encrypt(
_ message: Data,
key: some ContiguousBytes,
@@ -56,12 +80,22 @@ public struct Aes256GcmEncryptedData: Sendable {
return Self(nonce: nonce, ciphertext: ciphertext, authenticationTag: tag)
}
/// Encrypts the given plaintext using the given key, authenticating the ciphertext.
///
/// This API will generate a random nonce, which is included in the result.
///
/// - SeeAlso: ``encrypt(_:key:associatedData:)``
public static func encrypt(_ message: Data, key: some ContiguousBytes) throws -> Self {
return try self.encrypt(message, key: key, associatedData: [])
}
// Inlinable here specifically to avoid copying the ciphertext again if the struct is no longer used.
@inlinable
/// Decrypts `self` using the given key, and authenticates the ciphertext and given associated
/// data.
///
/// The associated data is not included in the ciphertext; instead, it's expected to match
/// between the encrypter and decrypter. If you don't have any extra data, use the other
/// overload ``decrypt(key:)``.
@inlinable // Inlinable here specifically to avoid copying the ciphertext again if the struct is no longer used.
public func decrypt(
key: some ContiguousBytes,
associatedData: some ContiguousBytes
@@ -75,14 +109,30 @@ public struct Aes256GcmEncryptedData: Sendable {
return plaintext
}
/// Decrypts `self` using the given key, authenticating the ciphertext.
///
/// - SeeAlso: ``decrypt(key:associatedData:)``
@inlinable
public func decrypt(key: some ContiguousBytes) throws -> Data {
return try self.decrypt(key: key, associatedData: [])
}
}
/// Supports streamed encryption and custom nonces. Use Aes256GcmEncryptedData if you don't need either.
/// Implements the
/// [AES-256-GCM](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM))
/// authenticated stream cipher with a 12-byte nonce.
///
/// This API exposes the streaming nature of AES-GCM to allow decrypting data without having it
/// resident in memory all at once. You must call ``computeTag()`` when the encryption is complete,
/// or else you have no authenticity guarantees. Use ``Aes256GcmEncryptedData`` instead if you don't
/// need to stream the data or choose your own nonce.
///
/// - SeeAlso: ``Aes256GcmDecryption``
public class Aes256GcmEncryption: NativeHandleOwner<SignalMutPointerAes256GcmEncryption> {
/// Initializes the cipher with the given inputs.
///
/// The associated data is not included in the plaintext or tag; instead, it's expected to match
/// between the encrypter and decrypter. If you don't need any extra data, pass an empty array.
public convenience init(
key: some ContiguousBytes,
nonce: some ContiguousBytes,
@@ -111,6 +161,9 @@ public class Aes256GcmEncryption: NativeHandleOwner<SignalMutPointerAes256GcmEnc
return signal_aes256_gcm_encryption_destroy(handle.pointer)
}
/// Encrypts `message` in place and advances the state of the cipher.
///
/// Don't forget to call ``computeTag()`` when encryption is complete.
public func encrypt(_ message: inout Data) throws {
try withNativeHandle { nativeHandle in
try message.withUnsafeMutableBytes { messageBytes in
@@ -126,6 +179,9 @@ public class Aes256GcmEncryption: NativeHandleOwner<SignalMutPointerAes256GcmEnc
}
}
/// Produces an authentication tag for the plaintext that has been processed.
///
/// After calling `computeTag()`, this object may not be used anymore.
public func computeTag() throws -> Data {
return try withNativeHandle { nativeHandle in
try invokeFnReturningData {
@@ -151,8 +207,21 @@ extension SignalMutPointerAes256GcmEncryption: SignalMutPointer {
}
}
/// Supports streamed decryption. Use Aes256GcmEncryptedData if you don't need streamed decryption.
/// Implements the
/// [AES-256-GCM](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM))
/// authenticated stream cipher with a 12-byte nonce.
///
/// This API exposes the streaming nature of AES-GCM to allow decrypting data without having it
/// resident in memory all at once. You **must** call ``verifyTag(_:)`` when the decryption is
/// complete, or else you have no authenticity guarantees. Use ``Aes256GcmEncryptedData`` instead if
/// you don't need streamed decryption.
///
/// - SeeAlso: ``Aes256GcmEncryption``
public class Aes256GcmDecryption: NativeHandleOwner<SignalMutPointerAes256GcmDecryption> {
/// Initializes the cipher with the given inputs.
///
/// The associated data is not included in the plaintext or tag; instead, it's expected to match
/// between the encrypter and decrypter.
public convenience init(
key: some ContiguousBytes,
nonce: some ContiguousBytes,
@@ -181,6 +250,9 @@ public class Aes256GcmDecryption: NativeHandleOwner<SignalMutPointerAes256GcmDec
return signal_aes256_gcm_decryption_destroy(handle.pointer)
}
/// Decrypts `message` in place and advances the state of the cipher.
///
/// Don't forget to call ``verifyTag(_:)`` when decryption is complete.
public func decrypt(_ message: inout Data) throws {
try withNativeHandle { nativeHandle in
try message.withUnsafeMutableBytes { messageBytes in
@@ -196,6 +268,12 @@ public class Aes256GcmDecryption: NativeHandleOwner<SignalMutPointerAes256GcmDec
}
}
/// Returns `true` if and only if `tag` matches the ciphertext that has been processed.
///
/// Throws if the tag is not structurally valid (which it's acceptable to treat as "did not
/// return `true`").
///
/// After calling `verifyTag(_:)`, this object may not be used anymore.
public func verifyTag(_ tag: some ContiguousBytes) throws -> Bool {
return try withNativeHandle { nativeHandle in
try tag.withUnsafeBorrowedBuffer { tagBuffer in

View File

@@ -6,6 +6,11 @@
import Foundation
import SignalFfi
/// Implements the [AES-256-GCM-SIV](https://en.wikipedia.org/wiki/AES-GCM-SIV)
/// authenticated stream cipher with a 12-byte nonce.
///
/// AES-GCM-SIV is a multi-pass algorithm (to generate the "synthetic initialization vector"), so
/// this API does not expose a streaming form.
public class Aes256GcmSiv: NativeHandleOwner<SignalMutPointerAes256GcmSiv> {
public convenience init<Bytes: ContiguousBytes>(key bytes: Bytes) throws {
let handle = try bytes.withUnsafeBorrowedBuffer { bytes in
@@ -22,6 +27,13 @@ public class Aes256GcmSiv: NativeHandleOwner<SignalMutPointerAes256GcmSiv> {
return signal_aes256_gcm_siv_destroy(handle.pointer)
}
/// Encrypts the given plaintext using the given nonce, and authenticating the ciphertext and given
/// associated data.
///
/// The associated data is not included in the ciphertext; instead, it's expected to match between
/// the encrypter and decrypter. If you don't need any extra data, pass an empty array.
///
/// - Returns: The encrypted data, including an appended 16-byte authentication tag.
public func encrypt(
_ message: some ContiguousBytes,
nonce: some ContiguousBytes,
@@ -46,6 +58,13 @@ public class Aes256GcmSiv: NativeHandleOwner<SignalMutPointerAes256GcmSiv> {
}
}
/// Decrypts the given ciphertext using the given nonce, and authenticating the ciphertext and
/// given associated data.
///
/// The associated data is not included in the ciphertext; instead, it's expected to match
/// between the encrypter and decrypter.
///
/// - Returns: The decrypted data
public func decrypt(
_ message: some ContiguousBytes,
nonce: some ContiguousBytes,