Repo cloned
This commit is contained in:
commit
496ae75f58
7988 changed files with 1451097 additions and 0 deletions
29
core-models/build.gradle.kts
Normal file
29
core-models/build.gradle.kts
Normal 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"))
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
288
core-models/src/main/java/org/signal/core/models/ServiceId.kt
Normal file
288
core-models/src/main/java/org/signal/core/models/ServiceId.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue