Our project follows the standard Kotlin coding conventions with the following changes
-
We allow up to 120 characters per line, please use all of the available space.
-
Only capitalize the first letter of words/acronyms/abbreviations/initialisms when using CamelCasing for example it's
UiTest, notUITest,IsoMdocType, notISOMDocType, and so on.
-
Never catch
Throwable:Throwableis the root of the hierarchy and includes fatal system errors (likeOutOfMemoryError). Catching it prevents the environment from terminating when it absolutely needs to, leading to corrupted state and unpredictable behavior. -
Don't throw
Error: Thejava.lang.Error(orkotlin.Errorwhich is type-aliased for when running on the JVM) class represents serious, unrecoverable problems that an application should generally not attempt to catch. Unlike standard exceptions, these typically indicate failures at the Java Virtual Machine (JVM) level or catastrophic system conditions. UseIllegalStateExceptionor similar instead. -
Avoid catching generic
Exception: Catching all exceptions acts as a black hole for standard developer bugs (likeNullPointerExceptionorIllegalArgumentException). This makes debugging extremely difficult because the application fails silently. -
Catch specific exceptions: Scope your
try-catchblocks tightly and only catch the exact exceptions you anticipate and know how to recover from (e.g.,IOExceptionfor network calls, orJsExceptionfor JavaScript interop boundaries). -
Protect Coroutine Cancellation: If you absolutely must catch a broad
Exception(e.g., at a top-level boundary to prevent a crash), you must explicitly rethrowCancellationException. Failing to do so intercepts coroutine cancellation, breaking structured concurrency and causing memory leaks.catch (e: Exception) { if (e is CancellationException) throw e // Handle other exceptions safely }
-
Avoid standard
runCatchingwith Coroutines: Kotlin's built-inrunCatching {}block catchesThrowableunder the hood. Do not use it around suspending functions unless you are using a custom wrapper that explicitly handlesCancellationException. -
Wrap
suspendcalls infinallyblocks: When a coroutine is cancelled, it throws aCancellationException. If you need to execute suspending cleanup code (like closing a connection or releasing a lock) inside afinallyblock, the coroutine is already in a cancelled state, and any standardsuspendcall will immediately fail. You must wrap the cleanup code inwithContext(NonCancellable).finally { withContext(NonCancellable) { // Suspending cleanup code goes here } }
-
Document exceptions in KDoc: Every function or method must explicitly document the exceptions it throws using the
@throws(or@exception) tag in its KDoc. This ensures that contributors and consumers of the Multipaz library know exactly what edge cases they are expected to handle. Also use the@Throwsannotation on the function or method since this is required for error handling when consuming the API on e.g. iOS.
-
Never use raw
ByteArrayfor keys in public APIs: Cryptographic keys must never be represented as rawByteArrayin public APIs. Symmetric cipher/MAC keys must useSecretKey(which inherits fromSecureByteString), and asymmetric private keys must usePrivateKey(or its specific subclasses such asEcPrivateKey,RsaPrivateKey, etc.). Key agreement functions (Crypto.keyAgreement,SecureArea.keyAgreement,AsymmetricKey.keyAgreement) and KEM decapsulation (Crypto.kemDecapsulate,SecureArea.kemDecapsulate) must returnSecureByteString(representing the raw shared secret) rather than rawByteArrayorSecretKey. Key derivation functions (Hkdf.deriveKey) acceptSecureByteStringas input keying material and returnSecretKey. Do not add convenience overloads takingByteArrayfor keys to public APIs (Crypto.encrypt,Crypto.mac,Hkdf.deriveKey,Cose.coseMac0, etc.). -
Always clear memory using
AutoCloseable/use:SecureByteString(and its subclassSecretKey), as well asPrivateKey, implementAutoCloseable(withclose()and aliasdestroy()). Whenever keys, shared secrets, or sensitive byte strings are created or used ephemerally, wrap them in Kotlin's.use {}block to ensure that the sensitive material is wiped from memory as soon as execution leaves the block, even if an exception or coroutine cancellation occurs:SecretKey(keyBytes).use { secretKey -> Crypto.encrypt(Algorithm.A128GCM, secretKey, nonce, plaintext) }
Or chaining directly with
keyAgreement():keyAgreement(otherPublicKey).use { sharedSecret -> Hkdf.deriveKey(Algorithm.HMAC_SHA256, sharedSecret, salt, info, 32).use { derivedKey -> // Use derivedKey } }If an object or service retains sensitive material long-term, the enclosing class should itself implement
AutoCloseableand destroy the data when closed. -
Explicitly zero raw byte arrays with
secureZero(): If raw key material or sensitive secrets exist in aByteArray(e.g., read from storage, decrypted from a network payload, or derived before wrapping intoSecretKeyorSecureByteString), you must explicitly clear it usingByteArray.secureZero()in afinallyblock:val rawKeyBytes = readKeyFromStorage() try { SecretKey(rawKeyBytes).use { secretKey -> // Use secretKey } } finally { rawKeyBytes.secureZero() }
Never use
ByteArray.fill(0): Standard array fills can be optimized away by the compiler or JVM/native JIT (dead store elimination). Always callByteArray.secureZero(), which uses platform-specific primitives that guarantee memory writes are not eliminated. -
Zero defensive copies returned by
encodedortoByteArray(): TheSecureByteString.encoded/toByteArray()property returns a defensive copy of the underlying bytes. The caller is responsible for callingsecureZero()on that copy once it is no longer needed. Internal SDK code should avoidencodedwhere possible, utilizing internal direct accessors or keeping the data withinSecureByteString/SecretKey. -
Avoid string representations of secrets: Secrets (passwords, PINs, raw keys) should never be held in immutable
Stringobjects where they cannot be wiped from memory. PreferByteArrayorCharSequencebuffers that can be zeroed out immediately after use. -
Random number generation and testability: Functions and classes that generate random data (salts, IVs/nonces, tokens, challenges, key identifiers, session IDs, disclosure digests, etc.) must accept a
random: Randomparameter (or constructor parameter) of typekotlin.random.Random. The parameter must have a default value:- Default to
Crypto.secureRandomif the generated data is security-sensitive (e.g. cryptographic keys, IVs, salts, nonces, session tokens, authorization codes, challenges, or privacy-preserving blinding factors). - Default to
Random.Defaultonly if the randomness is not security-sensitive (e.g. UI jitter, fuzz testing, or non-security tie-breaking). This design ensures that cryptographically secure randomness is always used by default in production, while unit and integration tests can inject a deterministically seededRandom(seed)instance to make cryptographic operations and protocol test vectors completely reproducible.
- Default to