mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Extract Media and TransformProperties to core/util/models.
This commit is contained in:
@@ -4,24 +4,16 @@
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("java-library")
|
||||
id("org.jetbrains.kotlin.jvm")
|
||||
id("ktlint")
|
||||
id("signal-library")
|
||||
id("kotlin-parcelize")
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.toVersion(libs.versions.javaVersion.get())
|
||||
targetCompatibility = JavaVersion.toVersion(libs.versions.javaVersion.get())
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion = JavaLanguageVersion.of(libs.versions.kotlinJvmTarget.get())
|
||||
}
|
||||
android {
|
||||
namespace = "org.signal.core.models"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.libsignal.client)
|
||||
implementation(libs.square.okio)
|
||||
implementation(project(":core:util-jvm"))
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.jackson.core)
|
||||
}
|
||||
|
||||
5
core/models/src/main/AndroidManifest.xml
Normal file
5
core/models/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* 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())
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* 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)"
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.models
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
/**
|
||||
* Kotlinx Serialization serializer for Android [Uri] objects.
|
||||
*/
|
||||
class UriSerializer : KSerializer<Uri> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Uri) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Uri {
|
||||
return Uri.parse(decoder.decodeString())
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.models.backup
|
||||
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "MediaId::${Hex.toStringCondensed(value)}"
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.models.backup
|
||||
|
||||
import org.signal.core.util.CryptoUtil
|
||||
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")
|
||||
fun forLocalBackupFilename(plaintextHash: ByteArray, localKey: ByteArray) = MediaName(Hex.toStringCondensed(CryptoUtil.sha256(plaintextHash + localKey)))
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* 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)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The AES key used to encrypt the backup id for local file backup metadata header.
|
||||
*/
|
||||
fun deriveLocalBackupMetadataKey(): ByteArray {
|
||||
return LibSignalBackupKey(value).deriveLocalBackupMetadataKey()
|
||||
}
|
||||
|
||||
class BackupKeyMaterial(
|
||||
val id: BackupId,
|
||||
val macKey: ByteArray,
|
||||
val aesKey: ByteArray
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.models.media
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.signal.core.models.UriSerializer
|
||||
|
||||
/**
|
||||
* Represents a piece of media that the user has on their device.
|
||||
*/
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class Media(
|
||||
@Serializable(with = UriSerializer::class) val uri: Uri,
|
||||
val contentType: String?,
|
||||
val date: Long,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val size: Long,
|
||||
val duration: Long,
|
||||
@get:JvmName("isBorderless") val isBorderless: Boolean,
|
||||
@get:JvmName("isVideoGif") val isVideoGif: Boolean,
|
||||
val bucketId: String?,
|
||||
val caption: String?,
|
||||
val transformProperties: TransformProperties?,
|
||||
var fileName: String?
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
const val ALL_MEDIA_BUCKET_ID: String = "org.thoughtcrime.securesms.ALL_MEDIA"
|
||||
}
|
||||
|
||||
fun withMimeType(newMimeType: String) = copy(contentType = newMimeType)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.models.media
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Properties that describe transformations to be applied to media before sending.
|
||||
*/
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class TransformProperties(
|
||||
@JsonProperty("skipTransform")
|
||||
@JvmField
|
||||
val skipTransform: Boolean = false,
|
||||
|
||||
@JsonProperty("videoTrim")
|
||||
@JvmField
|
||||
val videoTrim: Boolean = false,
|
||||
|
||||
@JsonProperty("videoTrimStartTimeUs")
|
||||
@JvmField
|
||||
val videoTrimStartTimeUs: Long = 0,
|
||||
|
||||
@JsonProperty("videoTrimEndTimeUs")
|
||||
@JvmField
|
||||
val videoTrimEndTimeUs: Long = 0,
|
||||
|
||||
@JsonProperty("sentMediaQuality")
|
||||
@JvmField
|
||||
val sentMediaQuality: Int = DEFAULT_MEDIA_QUALITY,
|
||||
|
||||
@JsonProperty("mp4Faststart")
|
||||
@JvmField
|
||||
val mp4FastStart: Boolean = false
|
||||
) : Parcelable {
|
||||
fun shouldSkipTransform(): Boolean {
|
||||
return skipTransform
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
@JsonProperty("videoEdited")
|
||||
val videoEdited: Boolean = videoTrim
|
||||
|
||||
fun withSkipTransform(): TransformProperties {
|
||||
return this.copy(
|
||||
skipTransform = true
|
||||
)
|
||||
}
|
||||
|
||||
fun withMp4FastStart(): TransformProperties {
|
||||
return this.copy(mp4FastStart = true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Corresponds to SentMediaQuality.STANDARD.code */
|
||||
const val DEFAULT_MEDIA_QUALITY = 0
|
||||
|
||||
@JvmStatic
|
||||
fun empty(): TransformProperties {
|
||||
return TransformProperties(
|
||||
skipTransform = false,
|
||||
videoTrim = false,
|
||||
videoTrimStartTimeUs = 0,
|
||||
videoTrimEndTimeUs = 0,
|
||||
sentMediaQuality = DEFAULT_MEDIA_QUALITY,
|
||||
mp4FastStart = false
|
||||
)
|
||||
}
|
||||
|
||||
fun forSkipTransform(): TransformProperties {
|
||||
return TransformProperties(
|
||||
skipTransform = true,
|
||||
videoTrim = false,
|
||||
videoTrimStartTimeUs = 0,
|
||||
videoTrimEndTimeUs = 0,
|
||||
sentMediaQuality = DEFAULT_MEDIA_QUALITY,
|
||||
mp4FastStart = false
|
||||
)
|
||||
}
|
||||
|
||||
fun forVideoTrim(videoTrimStartTimeUs: Long, videoTrimEndTimeUs: Long): TransformProperties {
|
||||
return TransformProperties(
|
||||
skipTransform = false,
|
||||
videoTrim = true,
|
||||
videoTrimStartTimeUs = videoTrimStartTimeUs,
|
||||
videoTrimEndTimeUs = videoTrimEndTimeUs,
|
||||
sentMediaQuality = DEFAULT_MEDIA_QUALITY,
|
||||
mp4FastStart = false
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forSentMediaQuality(sentMediaQuality: Int): TransformProperties {
|
||||
return TransformProperties(sentMediaQuality = sentMediaQuality)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.models.storageservice
|
||||
|
||||
interface StorageCipherKey {
|
||||
fun serialize(): ByteArray
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user