diff --git a/crates/core/src/crypto/encryption.rs b/crates/core/src/crypto/encryption.rs index d88ff08..054af6e 100644 --- a/crates/core/src/crypto/encryption.rs +++ b/crates/core/src/crypto/encryption.rs @@ -41,6 +41,20 @@ pub const SECRETSTREAM_ABYTES: usize = 17; /// `DECRYPTED_CHUNK_SIZE + SECRETSTREAM_ABYTES` (1024 + 17 = 1041). pub const ENCRYPTED_CHUNK_SIZE: usize = DECRYPTED_CHUNK_SIZE + SECRETSTREAM_ABYTES; +/// Return the exact ciphertext size produced by stream encryption. +/// +/// Returns `None` when the result cannot be represented as a `u64`. +pub fn encrypted_stream_size(plaintext_size: u64) -> Option { + if plaintext_size == 0 { + return Some(SECRETSTREAM_HEADER_SIZE as u64); + } + + let chunk_count = plaintext_size.div_ceil(DECRYPTED_CHUNK_SIZE as u64); + plaintext_size + .checked_add(SECRETSTREAM_HEADER_SIZE as u64)? + .checked_add(chunk_count.checked_mul(SECRETSTREAM_ABYTES as u64)?) +} + /// Encrypt a message using XSalsa20-Poly1305. /// /// # Wire Format @@ -550,6 +564,22 @@ mod tests { assert_eq!(decrypted, Vec::::new()); } + #[test] + fn test_encrypted_stream_size() { + for plaintext_size in [0, 1, 1024, 1025, 2048, 5000] { + let key = XChatConversationKey::from_bytes(vec![0x42u8; 32]).unwrap(); + let plaintext = vec![0xAB; plaintext_size]; + let mut encrypted = Vec::new(); + encrypt_stream(&key, Cursor::new(plaintext), &mut encrypted).unwrap(); + + assert_eq!( + encrypted_stream_size(plaintext_size as u64), + Some(encrypted.len() as u64) + ); + } + assert_eq!(encrypted_stream_size(u64::MAX), None); + } + #[test] fn test_encrypt_decrypt_stream_exact_chunk() { // Exactly one chunk (1024 bytes) — boundary case diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 8ef37de..26f6ccf 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -5,6 +5,13 @@ use base64::{engine::general_purpose::STANDARD, Engine as _}; +/// Return the exact ciphertext size produced by stream encryption. +/// +/// Returns `None` when the result cannot be represented as a `u64`. +pub fn encrypted_stream_size(plaintext_size: u64) -> Option { + crate::crypto::encryption::encrypted_stream_size(plaintext_size) +} + // Base64 Encoding/Decoding /// Encode bytes to base64 string. diff --git a/crates/dotnet/dotnet/ChatXdk.Tests/ChatTests.cs b/crates/dotnet/dotnet/ChatXdk.Tests/ChatTests.cs index 351e523..f9600b6 100644 --- a/crates/dotnet/dotnet/ChatXdk.Tests/ChatTests.cs +++ b/crates/dotnet/dotnet/ChatXdk.Tests/ChatTests.cs @@ -1293,6 +1293,16 @@ public void SignVerify_WrongPublicKey_ReturnsFalse() // ChatXdkUtilities (stateless helpers) + [Fact] + public void Utilities_EncryptedStreamSize() + { + Assert.Equal(24UL, ChatXdkUtilities.EncryptedStreamSize(0)); + Assert.Equal(42UL, ChatXdkUtilities.EncryptedStreamSize(1)); + Assert.Equal(1065UL, ChatXdkUtilities.EncryptedStreamSize(1024)); + Assert.Equal(1083UL, ChatXdkUtilities.EncryptedStreamSize(1025)); + Assert.Throws(() => ChatXdkUtilities.EncryptedStreamSize(ulong.MaxValue)); + } + [Fact] public void Utilities_Base64_RoundTrip() { diff --git a/crates/dotnet/dotnet/ChatXdk/ChatXdkUtilities.cs b/crates/dotnet/dotnet/ChatXdk/ChatXdkUtilities.cs index 1e7c694..623c5a3 100644 --- a/crates/dotnet/dotnet/ChatXdk/ChatXdkUtilities.cs +++ b/crates/dotnet/dotnet/ChatXdk/ChatXdkUtilities.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; @@ -40,6 +41,13 @@ private static byte[] Utf8Z(string s) return buf; } + /// Return the exact ciphertext size produced by stream encryption. + public static ulong EncryptedStreamSize(ulong plaintextSize) + { + string value = Consume(NativeMethods.chat_xdk_encrypted_stream_size(plaintextSize)); + return ulong.Parse(value, CultureInfo.InvariantCulture); + } + /// Encode bytes to standard base64. public static string BytesToBase64(ReadOnlySpan data) { diff --git a/crates/dotnet/dotnet/ChatXdk/NativeMethods.g.cs b/crates/dotnet/dotnet/ChatXdk/NativeMethods.g.cs index a1e0f1a..5ebac55 100644 --- a/crates/dotnet/dotnet/ChatXdk/NativeMethods.g.cs +++ b/crates/dotnet/dotnet/ChatXdk/NativeMethods.g.cs @@ -492,6 +492,12 @@ internal static unsafe partial class NativeMethods [DllImport(__DllName, EntryPoint = "chat_xdk_bytes_to_base64", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern FfiResult chat_xdk_bytes_to_base64(byte* data, nuint data_len); + /// + /// Return the exact ciphertext size produced by stream encryption. + /// + [DllImport(__DllName, EntryPoint = "chat_xdk_encrypted_stream_size", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern FfiResult chat_xdk_encrypted_stream_size(ulong plaintext_size); + /// /// Decode base64 to bytes. On success, `data` holds base64 of the decoded bytes (FFI-safe). /// diff --git a/crates/dotnet/src/lib.rs b/crates/dotnet/src/lib.rs index ac80662..2bb6f8c 100644 --- a/crates/dotnet/src/lib.rs +++ b/crates/dotnet/src/lib.rs @@ -1963,6 +1963,17 @@ pub extern "C" fn chat_xdk_bytes_to_base64(data: *const u8, data_len: usize) -> }) } +/// Return the exact ciphertext size produced by stream encryption. +#[no_mangle] +pub extern "C" fn chat_xdk_encrypted_stream_size(plaintext_size: u64) -> FfiResult { + catch_ffi( + || match chat_xdk_core::utils::encrypted_stream_size(plaintext_size) { + Some(size) => ok_data(&size.to_string()), + None => err_result("Encrypted stream size overflow"), + }, + ) +} + /// Decode base64 to bytes. On success, `data` holds base64 of the decoded bytes (FFI-safe). #[no_mangle] pub extern "C" fn chat_xdk_base64_to_bytes(b64: *const c_char) -> FfiResult { diff --git a/crates/go/src/lib.rs b/crates/go/src/lib.rs index 5a7ef86..941bb98 100644 --- a/crates/go/src/lib.rs +++ b/crates/go/src/lib.rs @@ -181,6 +181,17 @@ pub extern "C" fn chat_xdk_bytes_to_base64(data: *const u8, data_len: usize) -> }) } +/// Return the exact ciphertext size produced by stream encryption. +#[no_mangle] +pub extern "C" fn chat_xdk_encrypted_stream_size(plaintext_size: u64) -> FfiResult { + catch_ffi( + || match chat_xdk_core::utils::encrypted_stream_size(plaintext_size) { + Some(size) => ok_data(&size.to_string()), + None => err_result("Encrypted stream size overflow"), + }, + ) +} + /// Decode base64 to bytes. On success, `data` holds base64 of the decoded bytes (FFI-safe). #[no_mangle] pub extern "C" fn chat_xdk_base64_to_bytes(b64: *const c_char) -> FfiResult { diff --git a/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatNative.java b/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatNative.java index 027fdae..4abc49d 100644 --- a/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatNative.java +++ b/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatNative.java @@ -115,6 +115,8 @@ FfiResult.ByValue chat_xdk_decrypt_event( int chat_xdk_matches_registered_key(Pointer handle, Pointer publicKeyB64); + FfiResult.ByValue chat_xdk_encrypted_stream_size(long plaintextSize); + FfiResult.ByValue chat_xdk_bytes_to_base64(Pointer data, long dataLen); FfiResult.ByValue chat_xdk_base64_to_bytes(Pointer b64); diff --git a/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatXdkUtilities.java b/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatXdkUtilities.java index 77cf5b3..d29df3a 100644 --- a/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatXdkUtilities.java +++ b/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatXdkUtilities.java @@ -14,6 +14,20 @@ public final class ChatXdkUtilities { private ChatXdkUtilities() {} + /** Return the exact ciphertext size produced by stream encryption. */ + public static long encryptedStreamSize(long plaintextSize) { + if (plaintextSize < 0) { + throw new IllegalArgumentException("Plaintext size must be non-negative"); + } + String value = FfiStrings.consume( + ChatNative.INSTANCE.chat_xdk_encrypted_stream_size(plaintextSize)); + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + throw new ArithmeticException("Encrypted stream size exceeds Long.MAX_VALUE"); + } + } + /** Encode bytes to standard base64. */ public static String bytesToBase64(byte[] data) { if (data == null || data.length == 0) { diff --git a/crates/jvm/java/chatxdk/src/test/java/com/x/chatxdk/ChatTest.java b/crates/jvm/java/chatxdk/src/test/java/com/x/chatxdk/ChatTest.java index 07451b1..16be8d6 100644 --- a/crates/jvm/java/chatxdk/src/test/java/com/x/chatxdk/ChatTest.java +++ b/crates/jvm/java/chatxdk/src/test/java/com/x/chatxdk/ChatTest.java @@ -215,6 +215,18 @@ void utilitiesBase64RoundTrip() { assertArrayEquals(raw, ChatXdkUtilities.base64ToBytes(ChatXdkUtilities.bytesToBase64(raw))); } + @Test + void utilitiesEncryptedStreamSize() { + assertEquals(24, ChatXdkUtilities.encryptedStreamSize(0)); + assertEquals(42, ChatXdkUtilities.encryptedStreamSize(1)); + assertEquals(1065, ChatXdkUtilities.encryptedStreamSize(1024)); + assertEquals(1083, ChatXdkUtilities.encryptedStreamSize(1025)); + assertThrows(IllegalArgumentException.class, () -> ChatXdkUtilities.encryptedStreamSize(-1)); + assertThrows( + ArithmeticException.class, + () -> ChatXdkUtilities.encryptedStreamSize(Long.MAX_VALUE)); + } + @Test void exportImportRoundTrip() throws Exception { try (Chat chat = createUnlocked()) { diff --git a/crates/pyo3/python/chat_xdk/__init__.py b/crates/pyo3/python/chat_xdk/__init__.py index 5f349bc..dc7447d 100644 --- a/crates/pyo3/python/chat_xdk/__init__.py +++ b/crates/pyo3/python/chat_xdk/__init__.py @@ -39,6 +39,7 @@ bytes_to_hex, detect_image_dimensions, detect_mime_type, + encrypted_stream_size, hex_to_bytes, ) @@ -73,6 +74,7 @@ def guesses_remaining(exc): "bytes_to_hex", "detect_image_dimensions", "detect_mime_type", + "encrypted_stream_size", "guesses_remaining", "hex_to_bytes", ] diff --git a/crates/pyo3/python/chat_xdk/__init__.pyi b/crates/pyo3/python/chat_xdk/__init__.pyi index b366963..a2c9abc 100644 --- a/crates/pyo3/python/chat_xdk/__init__.pyi +++ b/crates/pyo3/python/chat_xdk/__init__.pyi @@ -4,6 +4,8 @@ from typing import Optional, Union, overload __version__: str +def encrypted_stream_size(plaintext_size: int) -> int: ... + # --------------------------------------------------------------------------- # Classes # --------------------------------------------------------------------------- diff --git a/crates/pyo3/python/tests/test_api.py b/crates/pyo3/python/tests/test_api.py index 9c5a058..baf650e 100644 --- a/crates/pyo3/python/tests/test_api.py +++ b/crates/pyo3/python/tests/test_api.py @@ -50,6 +50,16 @@ def test_base64_roundtrip(self): self.assertIsInstance(encoded, str) self.assertEqual(base64_to_bytes(encoded), data) + def test_encrypted_stream_size(self): + from chat_xdk import encrypted_stream_size + + self.assertEqual(encrypted_stream_size(0), 24) + self.assertEqual(encrypted_stream_size(1), 42) + self.assertEqual(encrypted_stream_size(1024), 1065) + self.assertEqual(encrypted_stream_size(1025), 1083) + with self.assertRaises(OverflowError): + encrypted_stream_size(2**64 - 1) + def test_base64_invalid_returns_none(self): from chat_xdk import base64_to_bytes diff --git a/crates/pyo3/src/lib.rs b/crates/pyo3/src/lib.rs index 05434dd..5116080 100644 --- a/crates/pyo3/src/lib.rs +++ b/crates/pyo3/src/lib.rs @@ -1510,6 +1510,13 @@ impl Chat { // Module-level utility functions +/// Return the exact ciphertext size produced by stream encryption. +#[pyfunction] +fn encrypted_stream_size(plaintext_size: u64) -> PyResult { + chat_xdk_core::utils::encrypted_stream_size(plaintext_size) + .ok_or_else(|| pyo3::exceptions::PyOverflowError::new_err("Encrypted stream size overflow")) +} + /// Encode bytes to base64 string. #[pyfunction] fn bytes_to_base64(bytes: &[u8]) -> String { @@ -1628,6 +1635,7 @@ fn _native(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> { m.add_class::()?; // Utility functions + m.add_function(wrap_pyfunction!(encrypted_stream_size, m)?)?; m.add_function(wrap_pyfunction!(bytes_to_base64, m)?)?; m.add_function(wrap_pyfunction!(base64_to_bytes, m)?)?; m.add_function(wrap_pyfunction!(bytes_to_hex, m)?)?; diff --git a/crates/wasm/js/index.d.ts b/crates/wasm/js/index.d.ts index caae779..a6fb289 100644 --- a/crates/wasm/js/index.d.ts +++ b/crates/wasm/js/index.d.ts @@ -1079,6 +1079,9 @@ export declare function createChat(options: CreateChatOptions): Promise encryptedStreamSize(-1), /non-negative safe integer/); + // hex roundtrip const hex = bytesToHex(someBytes); assert.equal(typeof hex, "string"); diff --git a/crates/wasm/js/tests/wrapper.test.mjs b/crates/wasm/js/tests/wrapper.test.mjs index 6b19ee2..2e7cfe6 100644 --- a/crates/wasm/js/tests/wrapper.test.mjs +++ b/crates/wasm/js/tests/wrapper.test.mjs @@ -8,7 +8,12 @@ import assert from "node:assert/strict"; import fs from "node:fs/promises"; import init, { Chat, bytesToBase64 } from "../pkg/chat_xdk_wasm.js"; -import { ChatWithJuicebox, createChat, guessesRemaining } from "../index.js"; +import { + ChatWithJuicebox, + createChat, + encryptedStreamSize, + guessesRemaining, +} from "../index.js"; // Node 18 exposes WebCrypto only via node:crypto; the wasm module's // random-byte source needs it on the global scope. @@ -668,6 +673,7 @@ async function realJuiceboxSingletonTests() { async function main() { await delegationTests(); + assert.equal(encryptedStreamSize(5000), 5109); await guessBudgetTests(); await firstBootTests(); await guessesRemainingTests(); diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs index 7cd3e5a..9fb99b2 100644 --- a/crates/wasm/src/lib.rs +++ b/crates/wasm/src/lib.rs @@ -41,6 +41,31 @@ use chat_xdk_core::js::{ // Utility Functions (free functions, not methods) +/// Return the exact ciphertext size produced by stream encryption. +#[wasm_bindgen(js_name = encryptedStreamSize)] +pub fn encrypted_stream_size(plaintext_size: f64) -> Result { + const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; + + if !plaintext_size.is_finite() + || plaintext_size < 0.0 + || plaintext_size.fract() != 0.0 + || plaintext_size > MAX_SAFE_INTEGER + { + return Err(JsError::new( + "Plaintext size must be a non-negative safe integer", + )); + } + + let encrypted_size = chat_xdk_core::utils::encrypted_stream_size(plaintext_size as u64) + .ok_or_else(|| JsError::new("Encrypted stream size overflow"))?; + if encrypted_size > MAX_SAFE_INTEGER as u64 { + return Err(JsError::new( + "Encrypted stream size exceeds Number.MAX_SAFE_INTEGER", + )); + } + Ok(encrypted_size as f64) +} + /// Encode bytes to base64 string. #[wasm_bindgen(js_name = bytesToBase64)] pub fn bytes_to_base64(bytes: &[u8]) -> String { diff --git a/docs/API.md b/docs/API.md index 65cde98..4041f80 100644 --- a/docs/API.md +++ b/docs/API.md @@ -923,6 +923,12 @@ Common helpers exported as module-level functions (Rust crate root / JS module / | 45 | **hex_to_bytes** | `(&str) → Option>` | `hexToBytes(string) → Uint8Array?` | `hex_to_bytes(str) → bytes?` | `HexToBytes(hex string) ([]byte, error)` | `ChatXdkUtilities.hexToBytes(String)` | `ChatXdkUtilities.HexToBytes(string)` | | 46 | **detect_mime_type** | `(&[u8]) → Option<&str>` | `detectMimeType(Uint8Array) → string?` | `detect_mime_type(bytes) → str?` | `DetectMimeType(data []byte) (mime string, err error)` — empty string if unknown | `ChatXdkUtilities.detectMimeType(byte[])` → `String` or `null` | `ChatXdkUtilities.DetectMimeType(ReadOnlySpan)` → `string?` | | 47 | **detect_image_dimensions** | `(&[u8]) → Option` | `detectImageDimensions(Uint8Array) → {width, height}?` | `detect_image_dimensions(bytes) → (w, h)?` | `DetectImageDimensions(data []byte) (*ImageDimensions, error)` — nil if unknown | `ChatXdkUtilities.detectImageDimensions(byte[])` → `ImageDimensions` or `null` | `ChatXdkUtilities.DetectImageDimensions(ReadOnlySpan)` → `ImageDimensions?` | +| 48 | **encrypted_stream_size** | `(plaintext_size: u64) → Option` | `encryptedStreamSize(plaintextSize: number) → number` | `encrypted_stream_size(plaintext_size: int) → int` | `EncryptedStreamSize(plaintextSize uint64) (uint64, error)` | `ChatXdkUtilities.encryptedStreamSize(long)` → `long` | `ChatXdkUtilities.EncryptedStreamSize(ulong)` → `ulong` | + +`encrypted_stream_size` returns the exact byte length that `encrypt_stream` +or `stream_encryptor` will produce, without encrypting or requiring a key: +`24 + plaintext_size + 17 × ceil(plaintext_size / 1024)` for non-empty input, +and `24` for empty input. **Failure modes differ by binding.** On undecodable input, `base64_to_bytes` / `hex_to_bytes` return `None` (Rust `Option`), `undefined` diff --git a/go/chatxdk/chatxdk_test.go b/go/chatxdk/chatxdk_test.go index 11e6f0b..a84ecbd 100644 --- a/go/chatxdk/chatxdk_test.go +++ b/go/chatxdk/chatxdk_test.go @@ -250,6 +250,21 @@ func TestLockAndUnlockState(t *testing.T) { } func TestUtilityHelpers(t *testing.T) { + for plaintextSize, want := range map[uint64]uint64{ + 0: 24, 1: 42, 1024: 1065, 1025: 1083, + } { + got, err := EncryptedStreamSize(plaintextSize) + if err != nil { + t.Fatalf("EncryptedStreamSize(%d): %v", plaintextSize, err) + } + if got != want { + t.Fatalf("EncryptedStreamSize(%d): got %d want %d", plaintextSize, got, want) + } + } + if _, err := EncryptedStreamSize(^uint64(0)); err == nil { + t.Fatal("expected overflow error") + } + // Base64 / hex roundtrip (parity with core utils tests) b64, err := BytesToBase64([]byte("Hello, World!")) if err != nil { diff --git a/go/chatxdk/ffi.go b/go/chatxdk/ffi.go index 832f03e..4600191 100644 --- a/go/chatxdk/ffi.go +++ b/go/chatxdk/ffi.go @@ -36,6 +36,10 @@ type handle = *C.struct_ChatHandle // utilities (stateless; no Chat handle) +func ffiEncryptedStreamSize(plaintextSize uint64) (string, error) { + return ffiResult(C.chat_xdk_encrypted_stream_size(C.uint64_t(plaintextSize))) +} + func ffiBytesToBase64(data []byte) (string, error) { var ptr *C.uint8_t if len(data) > 0 { diff --git a/go/chatxdk/include/chat_xdk.h b/go/chatxdk/include/chat_xdk.h index 005d3f7..45b2ed3 100644 --- a/go/chatxdk/include/chat_xdk.h +++ b/go/chatxdk/include/chat_xdk.h @@ -54,6 +54,11 @@ extern "C" { */ struct FfiResult chat_xdk_bytes_to_base64(const uint8_t *data, uintptr_t data_len); +/** + * Return the exact ciphertext size produced by stream encryption. + */ +struct FfiResult chat_xdk_encrypted_stream_size(uint64_t plaintext_size); + /** * Decode base64 to bytes. On success, `data` holds base64 of the decoded bytes (FFI-safe). */ diff --git a/go/chatxdk/libs/darwin_amd64/libchat_xdk_go.a b/go/chatxdk/libs/darwin_amd64/libchat_xdk_go.a index 83958ab..e4f5b0d 100644 Binary files a/go/chatxdk/libs/darwin_amd64/libchat_xdk_go.a and b/go/chatxdk/libs/darwin_amd64/libchat_xdk_go.a differ diff --git a/go/chatxdk/libs/darwin_arm64/libchat_xdk_go.a b/go/chatxdk/libs/darwin_arm64/libchat_xdk_go.a index 1b34ac3..c6c8997 100644 Binary files a/go/chatxdk/libs/darwin_arm64/libchat_xdk_go.a and b/go/chatxdk/libs/darwin_arm64/libchat_xdk_go.a differ diff --git a/go/chatxdk/libs/linux_amd64/libchat_xdk_go.a b/go/chatxdk/libs/linux_amd64/libchat_xdk_go.a index 32f8c00..4d1270b 100644 Binary files a/go/chatxdk/libs/linux_amd64/libchat_xdk_go.a and b/go/chatxdk/libs/linux_amd64/libchat_xdk_go.a differ diff --git a/go/chatxdk/libs/linux_amd64_musl/libchat_xdk_go.a b/go/chatxdk/libs/linux_amd64_musl/libchat_xdk_go.a index 5ab0250..c6806ec 100644 Binary files a/go/chatxdk/libs/linux_amd64_musl/libchat_xdk_go.a and b/go/chatxdk/libs/linux_amd64_musl/libchat_xdk_go.a differ diff --git a/go/chatxdk/utils.go b/go/chatxdk/utils.go index c3c5303..8cbf93b 100644 --- a/go/chatxdk/utils.go +++ b/go/chatxdk/utils.go @@ -3,6 +3,7 @@ package chatxdk import ( "encoding/base64" "encoding/json" + "strconv" ) // ImageDimensions is returned by [DetectImageDimensions] when a supported image header is recognized. @@ -11,6 +12,15 @@ type ImageDimensions struct { Height uint32 `json:"height"` } +// EncryptedStreamSize returns the exact ciphertext size produced by stream encryption. +func EncryptedStreamSize(plaintextSize uint64) (uint64, error) { + value, err := ffiEncryptedStreamSize(plaintextSize) + if err != nil { + return 0, err + } + return strconv.ParseUint(value, 10, 64) +} + // BytesToBase64 encodes bytes to standard base64. func BytesToBase64(data []byte) (string, error) { return ffiBytesToBase64(data)