Repo cloned

This commit is contained in:
Fr4nz D13trich 2025-12-29 13:18:34 +01:00
commit 496ae75f58
7988 changed files with 1451097 additions and 0 deletions

View file

@ -0,0 +1,29 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
val signalJavaVersion: JavaVersion by rootProject.extra
val signalKotlinJvmTarget: String by rootProject.extra
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
}
java {
sourceCompatibility = signalJavaVersion
targetCompatibility = signalJavaVersion
}
kotlin {
jvmToolchain {
languageVersion = JavaLanguageVersion.of(signalKotlinJvmTarget)
}
}
dependencies {
implementation(libs.libsignal.client)
implementation(libs.square.okio)
implementation(project(":core-util-jvm"))
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models
import org.signal.core.models.backup.MessageBackupKey
private typealias LibSignalAccountEntropyPool = org.signal.libsignal.messagebackup.AccountEntropyPool
/**
* The Root of All Entropy. You can use this to derive the [org.whispersystems.signalservice.api.kbs.MasterKey] or [org.whispersystems.signalservice.api.backup.MessageBackupKey].
*/
class AccountEntropyPool(value: String) {
val value = value.lowercase()
val displayValue = value.uppercase()
companion object {
private val INVALID_CHARACTERS = Regex("[^0-9a-zA-Z]")
const val LENGTH = 64
fun generate(): AccountEntropyPool {
return AccountEntropyPool(LibSignalAccountEntropyPool.generate())
}
fun parseOrNull(input: String): AccountEntropyPool? {
val stripped = removeIllegalCharacters(input)
if (stripped.length != LENGTH) {
return null
}
return AccountEntropyPool(stripped)
}
fun isFullyValid(input: String): Boolean {
return LibSignalAccountEntropyPool.isValid(input)
}
fun removeIllegalCharacters(input: String): String {
return input.replace(INVALID_CHARACTERS, "")
}
}
fun deriveMasterKey(): MasterKey {
return MasterKey(LibSignalAccountEntropyPool.deriveSvrKey(value))
}
fun deriveMessageBackupKey(): MessageBackupKey {
val libSignalBackupKey = LibSignalAccountEntropyPool.deriveBackupKey(value)
return MessageBackupKey(libSignalBackupKey.serialize())
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models
import org.signal.core.models.storageservice.StorageKey
import org.signal.core.util.Base64
import org.signal.core.util.CryptoUtil
import org.signal.core.util.Hex
import java.security.SecureRandom
class MasterKey(masterKey: ByteArray) {
private val masterKey: ByteArray
companion object {
private const val LENGTH = 32
fun createNew(secureRandom: SecureRandom): MasterKey {
val key = ByteArray(LENGTH)
secureRandom.nextBytes(key)
return MasterKey(key)
}
}
init {
check(masterKey.size == LENGTH) { "Master key must be $LENGTH bytes long (actualSize: ${masterKey.size})" }
this.masterKey = masterKey
}
fun deriveRegistrationLock(): String {
return Hex.toStringCondensed(derive("Registration Lock"))
}
fun deriveRegistrationRecoveryPassword(): String {
return Base64.encodeWithPadding(derive("Registration Recovery")!!)
}
fun deriveStorageServiceKey(): StorageKey {
return StorageKey(derive("Storage Service Encryption")!!)
}
fun deriveLoggingKey(): ByteArray? {
return derive("Logging Key")
}
private fun derive(keyName: String): ByteArray? {
return CryptoUtil.hmacSha256(masterKey, keyName.toByteArray(Charsets.UTF_8))
}
fun serialize(): ByteArray {
return masterKey.clone()
}
override fun equals(o: Any?): Boolean {
if (o == null || o.javaClass != javaClass) return false
return (o as MasterKey).masterKey.contentEquals(masterKey)
}
override fun hashCode(): Int {
return masterKey.contentHashCode()
}
override fun toString(): String {
return "MasterKey(xxx)"
}
}

View file

@ -0,0 +1,288 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.UuidUtil
import org.signal.core.util.toByteArray
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.logging.Log
import java.util.UUID
/**
* A wrapper around a UUID that represents an identifier for an account. Today, that is either an [ACI] or a [PNI].
* However, that doesn't mean every [ServiceId] is an *instance* of one of those classes. In reality, we often
* do not know which we have. And it shouldn't really matter.
*
* The only times you truly know, and the only times you should actually care, is during CDS refreshes or specific inbound messages
* that link them together.
*/
sealed class ServiceId(val libSignalServiceId: org.signal.libsignal.protocol.ServiceId) {
companion object {
private const val TAG = "ServiceId"
@JvmStatic
fun fromLibSignal(serviceId: org.signal.libsignal.protocol.ServiceId): ServiceId {
return when (serviceId) {
is org.signal.libsignal.protocol.ServiceId.Aci -> ACI(serviceId)
is org.signal.libsignal.protocol.ServiceId.Pni -> PNI(serviceId)
else -> throw IllegalArgumentException("Unknown libsignal ServiceId type!")
}
}
/** Parses a ServiceId serialized as a string. Returns null if the ServiceId is invalid. */
@JvmOverloads
@JvmStatic
fun parseOrNull(raw: String?, logFailures: Boolean = true): ServiceId? {
if (raw.isNullOrBlank()) {
return null
}
return try {
fromLibSignal(org.signal.libsignal.protocol.ServiceId.parseFromString(raw))
} catch (e: IllegalArgumentException) {
if (logFailures) {
Log.w(TAG, "[parseOrNull(String)] Illegal argument!", e)
}
null
} catch (e: org.signal.libsignal.protocol.ServiceId.InvalidServiceIdException) {
if (logFailures) {
Log.w(TAG, "[parseOrNull(String)] Invalid ServiceId!", e)
}
null
}
}
/** Parses a ServiceId serialized as a byte array. Returns null if the ServiceId is invalid. */
@JvmStatic
fun parseOrNull(raw: ByteArray?): ServiceId? {
if (raw == null || raw.isEmpty()) {
return null
}
return try {
if (raw.size == 17) {
fromLibSignal(org.signal.libsignal.protocol.ServiceId.parseFromFixedWidthBinary(raw))
} else {
fromLibSignal(org.signal.libsignal.protocol.ServiceId.parseFromBinary(raw))
}
} catch (e: IllegalArgumentException) {
Log.w(TAG, "[parseOrNull(Bytes)] Illegal argument!", e)
null
} catch (e: org.signal.libsignal.protocol.ServiceId.InvalidServiceIdException) {
Log.w(TAG, "[parseOrNull(Bytes)] Invalid ServiceId!", e)
null
}
}
/** Parses a ServiceId serialized as a ByteString. Returns null if the ServiceId is invalid. */
@JvmStatic
fun parseOrNull(bytes: ByteString?): ServiceId? = parseOrNull(bytes?.toByteArray())
/** Parses a ServiceId serialized as a string. Crashes if the ServiceId is invalid. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(raw: String?): ServiceId = parseOrNull(raw) ?: throw IllegalArgumentException("Invalid ServiceId!")
/** Parses a ServiceId serialized as a byte array. Crashes if the ServiceId is invalid. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(raw: ByteArray): ServiceId = parseOrNull(raw) ?: throw IllegalArgumentException("Invalid ServiceId!")
/** Parses a ServiceId serialized as a ByteString. Crashes if the ServiceId is invalid. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(bytes: ByteString): ServiceId = parseOrThrow(bytes.toByteArray())
/** Parses a ServiceId serialized as a ByteString. Returns [ACI.UNKNOWN] if not parseable. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrUnknown(bytes: ByteString): ServiceId {
return parseOrNull(bytes) ?: ACI.UNKNOWN
}
/** Parses a ServiceId serialized as either a byteString or string, with preference to the byteString if available. Returns null if invalid. */
@JvmStatic
fun parseOrNull(raw: String?, bytes: ByteString?): ServiceId? {
return parseOrNull(bytes) ?: parseOrNull(raw)
}
/** Parses a ServiceId serialized as either a byteString or string, with preference to the byteString if available. Throws if invalid. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(raw: String?, bytes: ByteString?): ServiceId {
return parseOrNull(bytes) ?: parseOrThrow(raw)
}
}
val rawUuid: UUID = libSignalServiceId.rawUUID
val isUnknown: Boolean = rawUuid == UuidUtil.UNKNOWN_UUID
val isValid: Boolean = !isUnknown
fun toProtocolAddress(deviceId: Int): SignalProtocolAddress = SignalProtocolAddress(libSignalServiceId.toServiceIdString(), deviceId)
fun toByteString(): ByteString = ByteString.Companion.of(*libSignalServiceId.toServiceIdBinary())
fun toByteArray(): ByteArray = libSignalServiceId.toServiceIdBinary()
fun logString(): String = libSignalServiceId.toLogString()
/**
* A serialized string that can be parsed via [parseOrThrow], for instance.
* Basically ACI's are just normal UUIDs, and PNI's are UUIDs with a `PNI:` prefix.
*/
override fun toString(): String = libSignalServiceId.toServiceIdString()
data class ACI(val libSignalAci: org.signal.libsignal.protocol.ServiceId.Aci) : ServiceId(libSignalAci) {
companion object {
@JvmField
val UNKNOWN = from(UuidUtil.UNKNOWN_UUID)
@JvmStatic
fun from(uuid: UUID): ACI = ACI(org.signal.libsignal.protocol.ServiceId.Aci(uuid))
@JvmStatic
fun fromLibSignal(aci: org.signal.libsignal.protocol.ServiceId.Aci): ACI = ACI(aci)
@JvmStatic
fun parseOrNull(raw: String?): ACI? = ServiceId.parseOrNull(raw).let { it as? ACI }
@JvmStatic
fun parseOrNull(raw: ByteArray?): ACI? = ServiceId.parseOrNull(raw).let { it as? ACI }
@JvmStatic
fun parseOrNull(bytes: ByteString?): ACI? = parseOrNull(bytes?.toByteArray())
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(raw: String?): ACI = parseOrNull(raw) ?: throw IllegalArgumentException("Invalid ACI!")
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(raw: ByteArray?): ACI = parseOrNull(raw) ?: throw IllegalArgumentException("Invalid ACI!")
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(bytes: ByteString): ACI = parseOrThrow(bytes.toByteArray())
@JvmStatic
fun parseOrUnknown(bytes: ByteString?): ACI = UuidUtil.fromByteStringOrNull(bytes)?.let { from(it) } ?: UNKNOWN
@JvmStatic
fun parseOrUnknown(raw: String?): ACI = parseOrNull(raw) ?: UNKNOWN
/** Parses either a byteString or string as an ACI, with preference to the byteString if available. Returns null if invalid or missing. */
@JvmStatic
fun parseOrNull(raw: String?, bytes: ByteString?): ACI? {
return parseOrNull(bytes) ?: parseOrNull(raw)
}
/** Parses either a byteString or string as an ACI, with preference to the byteString if available. Throws if invalid or missing. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(raw: String?, bytes: ByteString?): ACI {
return parseOrNull(bytes) ?: parseOrThrow(raw)
}
}
override fun toString(): String = super.toString()
}
data class PNI(val libSignalPni: org.signal.libsignal.protocol.ServiceId.Pni) : ServiceId(libSignalPni) {
companion object {
@JvmField
var UNKNOWN = from(UuidUtil.UNKNOWN_UUID)
@JvmStatic
fun from(uuid: UUID): PNI = PNI(org.signal.libsignal.protocol.ServiceId.Pni(uuid))
/** Parses a string as a PNI, regardless if the `PNI:` prefix is present or not. Only use this if you are certain that what you're reading is a PNI. */
@JvmStatic
fun parseOrNull(raw: String?): PNI? {
return if (raw == null) {
null
} else if (raw.startsWith("PNI:")) {
return parsePrefixedOrNull(raw)
} else {
val uuid = UuidUtil.parseOrNull(raw)
if (uuid != null) {
PNI(org.signal.libsignal.protocol.ServiceId.Pni(uuid))
} else {
null
}
}
}
/** Parse a byte array as a PNI, regardless if it has the type prefix byte present or not. Only use this if you are certain what you're reading is a PNI. */
@JvmStatic
fun parseOrNull(raw: ByteArray?): PNI? {
return if (raw == null || raw.isEmpty()) {
null
} else if (raw.size == 17) {
ServiceId.parseOrNull(raw).let { if (it is PNI) it else null }
} else {
val uuid = UuidUtil.parseOrNull(raw)
if (uuid != null) {
PNI(org.signal.libsignal.protocol.ServiceId.Pni(uuid))
} else {
null
}
}
}
/** Parses a [ByteString] as a PNI, regardless if the `PNI:` prefix is present or not. Only use this if you are certain that what you're reading is a PNI. */
@JvmStatic
fun parseOrNull(bytes: ByteString?): PNI? = parseOrNull(bytes?.toByteArray())
/** Parses a string as a PNI, regardless if the `PNI:` prefix is present or not. Only use this if you are certain that what you're reading is a PNI. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(raw: String?): PNI = parseOrNull(raw) ?: throw IllegalArgumentException("Invalid PNI!")
/** Parse a byte array as a PNI, regardless if it has the type prefix byte present or not. Only use this if you are certain what you're reading is a PNI. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(raw: ByteArray?): PNI = parseOrNull(raw) ?: throw IllegalArgumentException("Invalid PNI!")
/** Parse a byte string as a PNI, regardless if it has the type prefix byte present or not. Only use this if you are certain what you're reading is a PNI. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(bytes: ByteString): PNI = parseOrThrow(bytes.toByteArray())
/** Parses a string as a PNI, expecting that the value has a `PNI:` prefix. If it does not have the prefix (or is otherwise invalid), this will return null. */
fun parsePrefixedOrNull(raw: String?): PNI? = ServiceId.parseOrNull(raw).let { if (it is PNI) it else null }
/** Parses either a byteString or string as a PNI, with preference to the byteString. Expecting that the value has a `PNI:` prefix. If it does not have the prefix (or is otherwise invalid), this will return null. */
fun parsePrefixedOrNull(raw: String?, bytes: ByteString?): PNI? {
return parseOrNull(bytes).let { if (it is PNI) it else null } ?: parsePrefixedOrNull(raw)
}
/** Parses either a byteString or string as a PNI, with preference to the byteString. Only use this if you are certain what you're reading is a PNI. Returns null if invalid. */
@JvmStatic
fun parseOrNull(raw: String?, bytes: ByteString?): PNI? {
return parseOrNull(bytes) ?: parseOrNull(raw)
}
/** Parses either a byteString or string as a PNI, with preference to the byteString. Only use this if you are certain what you're reading is a PNI. Throws if missing or invalid. */
@JvmStatic
@Throws(IllegalArgumentException::class)
fun parseOrThrow(raw: String?, bytes: ByteString?): PNI {
return parseOrNull(bytes) ?: parseOrThrow(raw)
}
}
override fun toString(): String = super.toString()
/** String version without the PNI: prefix. This is only for specific proto fields. For application storage, prefer [toString]. */
fun toStringWithoutPrefix(): String = rawUuid.toString()
/** [ByteString] version without the PNI byte prefix. */
fun toByteStringWithoutPrefix(): ByteString = rawUuid.toByteArray().toByteString()
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.backup
import org.signal.core.util.Base64
import java.security.MessageDigest
/**
* Safe typing around a backupId, which is a 16-byte array.
*/
@JvmInline
value class BackupId(val value: ByteArray) {
init {
require(value.size == 16) { "BackupId must be 16 bytes!" }
}
/** Encode backup-id for use in a URL/request */
fun encode(): String {
return Base64.encodeUrlSafeWithPadding(MessageDigest.getInstance("SHA-256").digest(value).copyOfRange(0, 16))
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.backup
import org.signal.core.models.ServiceId
import org.signal.libsignal.protocol.ecc.ECPrivateKey
/**
* Contains the common properties for all "backup keys", namely the [MessageBackupKey] and [org.whispersystems.signalservice.api.backup.MediaRootBackupKey]
*/
interface BackupKey {
val value: ByteArray
/**
* The private key used to generate anonymous credentials when interacting with the backup service.
*/
fun deriveAnonymousCredentialPrivateKey(aci: ServiceId.ACI): ECPrivateKey
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.backup
import org.signal.core.util.Base64
/**
* Safe typing around a mediaId, which is a 15-byte array.
*/
@JvmInline
value class MediaId(val value: ByteArray) {
constructor(mediaId: String) : this(Base64.decode(mediaId))
init {
require(value.size == 15) { "MediaId must be 15 bytes!" }
}
/** Encode media-id for use in a URL/request */
fun encode(): String {
return Base64.encodeUrlSafeWithPadding(value)
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.backup
import org.signal.core.util.Hex
/**
* Represent a media name for the various types of media that can be archived.
*/
@JvmInline
value class MediaName(val name: String) {
companion object {
fun fromPlaintextHashAndRemoteKey(plaintextHash: ByteArray, remoteKey: ByteArray) = MediaName(Hex.toStringCondensed(plaintextHash + remoteKey))
fun fromPlaintextHashAndRemoteKeyForThumbnail(plaintextHash: ByteArray, remoteKey: ByteArray) = MediaName(Hex.toStringCondensed(plaintextHash + remoteKey) + "_thumbnail")
fun forThumbnailFromMediaName(mediaName: String) = MediaName("${mediaName}_thumbnail")
/**
* For java, since it struggles with value classes.
*/
@JvmStatic
fun toMediaIdString(mediaName: String, mediaRootBackupKey: MediaRootBackupKey): String {
return MediaName(mediaName).toMediaId(mediaRootBackupKey).encode()
}
}
fun toMediaId(mediaRootBackupKey: MediaRootBackupKey): MediaId {
return mediaRootBackupKey.deriveMediaId(this)
}
override fun toString(): String {
return name
}
}

View file

@ -0,0 +1,86 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.backup
import org.signal.core.models.ServiceId
import org.signal.core.util.RandomUtil
import org.signal.libsignal.protocol.ecc.ECPrivateKey
/**
* Safe typing around a media root backup key, which is a 32-byte array.
* This key is a purely random value.
*/
class MediaRootBackupKey(override val value: ByteArray) : BackupKey {
companion object {
fun generate(): MediaRootBackupKey {
return MediaRootBackupKey(RandomUtil.getSecureBytes(32))
}
}
/**
* The private key used to generate anonymous credentials when interacting with the backup service.
*/
override fun deriveAnonymousCredentialPrivateKey(aci: ServiceId.ACI): ECPrivateKey {
return org.signal.libsignal.messagebackup.BackupKey(value).deriveEcKey(aci.libSignalAci)
}
fun deriveMediaId(mediaName: MediaName): MediaId {
return MediaId(org.signal.libsignal.messagebackup.BackupKey(value).deriveMediaId(mediaName.name))
}
fun deriveMediaSecrets(mediaName: MediaName): MediaKeyMaterial {
val mediaId = deriveMediaId(mediaName)
return deriveMediaSecrets(mediaId)
}
fun deriveMediaSecretsFromMediaId(base64MediaId: String): MediaKeyMaterial {
return deriveMediaSecrets(MediaId(base64MediaId))
}
fun deriveThumbnailTransitKey(thumbnailMediaName: MediaName): ByteArray {
return org.signal.libsignal.messagebackup.BackupKey(value).deriveThumbnailTransitEncryptionKey(deriveMediaId(thumbnailMediaName).value)
}
private fun deriveMediaSecrets(mediaId: MediaId): MediaKeyMaterial {
val libsignalBackupKey = org.signal.libsignal.messagebackup.BackupKey(value)
val combinedKey = libsignalBackupKey.deriveMediaEncryptionKey(mediaId.value)
return MediaKeyMaterial(
id = mediaId,
macKey = combinedKey.copyOfRange(0, 32),
aesKey = combinedKey.copyOfRange(32, 64)
)
}
/**
* Identifies a the location of a user's backup.
*/
fun deriveBackupId(aci: ServiceId.ACI): BackupId {
return BackupId(
org.signal.libsignal.messagebackup.BackupKey(value).deriveBackupId(aci.libSignalAci)
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MediaRootBackupKey
return value.contentEquals(other.value)
}
override fun hashCode(): Int {
return value.contentHashCode()
}
class MediaKeyMaterial(
val id: MediaId,
val macKey: ByteArray,
val aesKey: ByteArray
)
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.backup
import org.signal.core.models.ServiceId
import org.signal.libsignal.messagebackup.BackupForwardSecrecyToken
import org.signal.libsignal.messagebackup.MessageBackupKey
import org.signal.libsignal.protocol.ecc.ECPrivateKey
private typealias LibSignalBackupKey = org.signal.libsignal.messagebackup.BackupKey
/**
* Safe typing around a backup key, which is a 32-byte array.
* This key is derived from the AEP.
*/
class MessageBackupKey(override val value: ByteArray) : BackupKey {
init {
require(value.size == 32) { "Backup key must be 32 bytes!" }
}
/**
* The private key used to generate anonymous credentials when interacting with the backup service.
*/
override fun deriveAnonymousCredentialPrivateKey(aci: ServiceId.ACI): ECPrivateKey {
return LibSignalBackupKey(value).deriveEcKey(aci.libSignalAci)
}
/**
* The cryptographic material used to encrypt a backup.
*
* @param forwardSecrecyToken Should be present for any backup located on the archive CDN. Absent for other uses (i.e. link+sync).
*/
fun deriveBackupSecrets(aci: ServiceId.ACI, forwardSecrecyToken: BackupForwardSecrecyToken?): BackupKeyMaterial {
val backupId = deriveBackupId(aci)
val libsignalBackupKey = LibSignalBackupKey(value)
val libsignalMessageMessageBackupKey = MessageBackupKey(libsignalBackupKey, backupId.value, forwardSecrecyToken)
return BackupKeyMaterial(
id = backupId,
macKey = libsignalMessageMessageBackupKey.hmacKey,
aesKey = libsignalMessageMessageBackupKey.aesKey
)
}
/**
* Identifies a the location of a user's backup.
*/
fun deriveBackupId(aci: ServiceId.ACI): BackupId {
return BackupId(
LibSignalBackupKey(value).deriveBackupId(aci.libSignalAci)
)
}
class BackupKeyMaterial(
val id: BackupId,
val macKey: ByteArray,
val aesKey: ByteArray
)
}

View file

@ -0,0 +1,10 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.storageservice
interface StorageCipherKey {
fun serialize(): ByteArray
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.storageservice
/**
* Key used to encrypt individual storage items in the storage service.
*
* Created via [StorageKey.deriveItemKey].
*/
class StorageItemKey(val key: ByteArray) : StorageCipherKey {
init {
check(key.size == 32)
}
override fun serialize(): ByteArray = key.clone()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as StorageItemKey
return key.contentEquals(other.key)
}
override fun hashCode(): Int {
return key.contentHashCode()
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.storageservice
import org.signal.core.util.Base64
import org.signal.core.util.CryptoUtil
/**
* Key used to encrypt data on the storage service. Not used directly -- instead we used keys that
* are derived for each item we're storing.
*
* Created via [org.signal.core.models.MasterKey.deriveStorageServiceKey].
*/
class StorageKey(val key: ByteArray) {
init {
check(key.size == 32)
}
fun deriveManifestKey(version: Long): StorageManifestKey {
return StorageManifestKey(derive("Manifest_$version"))
}
fun deriveItemKey(rawId: ByteArray): StorageItemKey {
return StorageItemKey(derive("Item_" + Base64.encodeWithPadding(rawId)))
}
private fun derive(keyName: String): ByteArray {
return CryptoUtil.hmacSha256(key, keyName.toByteArray(Charsets.UTF_8))
}
fun serialize(): ByteArray {
return key.clone()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as StorageKey
return key.contentEquals(other.key)
}
override fun hashCode(): Int {
return key.contentHashCode()
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.models.storageservice
/**
* Key used to encrypt a manifest in the storage service.
*
* Created via [org.whispersystems.signalservice.api.storage.StorageKey.deriveManifestKey].
*/
class StorageManifestKey(val key: ByteArray) : StorageCipherKey {
init {
check(key.size == 32)
}
override fun serialize(): ByteArray = key.clone()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as StorageManifestKey
return key.contentEquals(other.key)
}
override fun hashCode(): Int {
return key.contentHashCode()
}
}