1 package org
.asamk
.signal
.manager
.storage
;
3 import com
.zaxxer
.hikari
.HikariDataSource
;
5 import org
.asamk
.signal
.manager
.api
.Pair
;
6 import org
.asamk
.signal
.manager
.storage
.groups
.GroupStore
;
7 import org
.asamk
.signal
.manager
.storage
.identities
.IdentityKeyStore
;
8 import org
.asamk
.signal
.manager
.storage
.prekeys
.KyberPreKeyStore
;
9 import org
.asamk
.signal
.manager
.storage
.prekeys
.PreKeyStore
;
10 import org
.asamk
.signal
.manager
.storage
.prekeys
.SignedPreKeyStore
;
11 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientStore
;
12 import org
.asamk
.signal
.manager
.storage
.sendLog
.MessageSendLogStore
;
13 import org
.asamk
.signal
.manager
.storage
.senderKeys
.SenderKeyRecordStore
;
14 import org
.asamk
.signal
.manager
.storage
.senderKeys
.SenderKeySharedStore
;
15 import org
.asamk
.signal
.manager
.storage
.sessions
.SessionStore
;
16 import org
.asamk
.signal
.manager
.storage
.stickers
.StickerStore
;
17 import org
.slf4j
.Logger
;
18 import org
.slf4j
.LoggerFactory
;
19 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
20 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
21 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
24 import java
.sql
.Connection
;
25 import java
.sql
.SQLException
;
26 import java
.util
.HashMap
;
27 import java
.util
.Optional
;
28 import java
.util
.UUID
;
30 public class AccountDatabase
extends Database
{
32 private final static Logger logger
= LoggerFactory
.getLogger(AccountDatabase
.class);
33 private static final long DATABASE_VERSION
= 16;
35 private AccountDatabase(final HikariDataSource dataSource
) {
36 super(logger
, DATABASE_VERSION
, dataSource
);
39 public static AccountDatabase
init(File databaseFile
) throws SQLException
{
40 return initDatabase(databaseFile
, AccountDatabase
::new);
44 protected void createDatabase(final Connection connection
) throws SQLException
{
45 RecipientStore
.createSql(connection
);
46 MessageSendLogStore
.createSql(connection
);
47 StickerStore
.createSql(connection
);
48 PreKeyStore
.createSql(connection
);
49 SignedPreKeyStore
.createSql(connection
);
50 KyberPreKeyStore
.createSql(connection
);
51 GroupStore
.createSql(connection
);
52 SessionStore
.createSql(connection
);
53 IdentityKeyStore
.createSql(connection
);
54 SenderKeyRecordStore
.createSql(connection
);
55 SenderKeySharedStore
.createSql(connection
);
59 protected void upgradeDatabase(final Connection connection
, final long oldVersion
) throws SQLException
{
61 logger
.debug("Updating database: Creating recipient table");
62 try (final var statement
= connection
.createStatement()) {
63 statement
.executeUpdate("""
64 CREATE TABLE recipient (
65 _id INTEGER PRIMARY KEY AUTOINCREMENT,
69 profile_key_credential BLOB,
75 expiration_time INTEGER NOT NULL DEFAULT 0,
76 blocked INTEGER NOT NULL DEFAULT FALSE,
77 archived INTEGER NOT NULL DEFAULT FALSE,
78 profile_sharing INTEGER NOT NULL DEFAULT FALSE,
80 profile_last_update_timestamp INTEGER NOT NULL DEFAULT 0,
81 profile_given_name TEXT,
82 profile_family_name TEXT,
84 profile_about_emoji TEXT,
85 profile_avatar_url_path TEXT,
86 profile_mobile_coin_address BLOB,
87 profile_unidentified_access_mode TEXT,
88 profile_capabilities TEXT
94 logger
.debug("Updating database: Creating sticker table");
95 try (final var statement
= connection
.createStatement()) {
96 statement
.executeUpdate("""
97 CREATE TABLE sticker (
98 _id INTEGER PRIMARY KEY,
99 pack_id BLOB UNIQUE NOT NULL,
100 pack_key BLOB NOT NULL,
101 installed INTEGER NOT NULL DEFAULT FALSE
106 if (oldVersion
< 4) {
107 logger
.debug("Updating database: Creating pre key tables");
108 try (final var statement
= connection
.createStatement()) {
109 statement
.executeUpdate("""
110 CREATE TABLE signed_pre_key (
111 _id INTEGER PRIMARY KEY,
112 account_id_type INTEGER NOT NULL,
113 key_id INTEGER NOT NULL,
114 public_key BLOB NOT NULL,
115 private_key BLOB NOT NULL,
116 signature BLOB NOT NULL,
117 timestamp INTEGER DEFAULT 0,
118 UNIQUE(account_id_type, key_id)
120 CREATE TABLE pre_key (
121 _id INTEGER PRIMARY KEY,
122 account_id_type INTEGER NOT NULL,
123 key_id INTEGER NOT NULL,
124 public_key BLOB NOT NULL,
125 private_key BLOB NOT NULL,
126 UNIQUE(account_id_type, key_id)
131 if (oldVersion
< 5) {
132 logger
.debug("Updating database: Creating group tables");
133 try (final var statement
= connection
.createStatement()) {
134 statement
.executeUpdate("""
135 CREATE TABLE group_v2 (
136 _id INTEGER PRIMARY KEY,
137 group_id BLOB UNIQUE NOT NULL,
138 master_key BLOB NOT NULL,
140 distribution_id BLOB UNIQUE NOT NULL,
141 blocked INTEGER NOT NULL DEFAULT FALSE,
142 permission_denied INTEGER NOT NULL DEFAULT FALSE
144 CREATE TABLE group_v1 (
145 _id INTEGER PRIMARY KEY,
146 group_id BLOB UNIQUE NOT NULL,
147 group_id_v2 BLOB UNIQUE,
150 expiration_time INTEGER NOT NULL DEFAULT 0,
151 blocked INTEGER NOT NULL DEFAULT FALSE,
152 archived INTEGER NOT NULL DEFAULT FALSE
154 CREATE TABLE group_v1_member (
155 _id INTEGER PRIMARY KEY,
156 group_id INTEGER NOT NULL REFERENCES group_v1 (_id) ON DELETE CASCADE,
157 recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
158 UNIQUE(group_id, recipient_id)
163 if (oldVersion
< 6) {
164 logger
.debug("Updating database: Creating session tables");
165 try (final var statement
= connection
.createStatement()) {
166 statement
.executeUpdate("""
167 CREATE TABLE session (
168 _id INTEGER PRIMARY KEY,
169 account_id_type INTEGER NOT NULL,
170 recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
171 device_id INTEGER NOT NULL,
172 record BLOB NOT NULL,
173 UNIQUE(account_id_type, recipient_id, device_id)
178 if (oldVersion
< 7) {
179 logger
.debug("Updating database: Creating identity table");
180 try (final var statement
= connection
.createStatement()) {
181 statement
.executeUpdate("""
182 CREATE TABLE identity (
183 _id INTEGER PRIMARY KEY,
184 recipient_id INTEGER UNIQUE NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
185 identity_key BLOB NOT NULL,
186 added_timestamp INTEGER NOT NULL,
187 trust_level INTEGER NOT NULL
192 if (oldVersion
< 8) {
193 logger
.debug("Updating database: Creating sender key tables");
194 try (final var statement
= connection
.createStatement()) {
195 statement
.executeUpdate("""
196 CREATE TABLE sender_key (
197 _id INTEGER PRIMARY KEY,
198 recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
199 device_id INTEGER NOT NULL,
200 distribution_id BLOB NOT NULL,
201 record BLOB NOT NULL,
202 created_timestamp INTEGER NOT NULL,
203 UNIQUE(recipient_id, device_id, distribution_id)
205 CREATE TABLE sender_key_shared (
206 _id INTEGER PRIMARY KEY,
207 recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
208 device_id INTEGER NOT NULL,
209 distribution_id BLOB NOT NULL,
210 timestamp INTEGER NOT NULL,
211 UNIQUE(recipient_id, device_id, distribution_id)
216 if (oldVersion
< 9) {
217 logger
.debug("Updating database: Adding urgent field");
218 try (final var statement
= connection
.createStatement()) {
219 statement
.executeUpdate("""
220 ALTER TABLE message_send_log_content ADD COLUMN urgent INTEGER NOT NULL DEFAULT TRUE;
224 if (oldVersion
< 10) {
225 logger
.debug("Updating database: Key tables on serviceId instead of recipientId");
226 try (final var statement
= connection
.createStatement()) {
227 statement
.executeUpdate("""
228 CREATE TABLE identity2 (
229 _id INTEGER PRIMARY KEY,
230 uuid BLOB UNIQUE NOT NULL,
231 identity_key BLOB NOT NULL,
232 added_timestamp INTEGER NOT NULL,
233 trust_level INTEGER NOT NULL
235 INSERT INTO identity2 (_id, uuid, identity_key, added_timestamp, trust_level)
236 SELECT i._id, r.uuid, i.identity_key, i.added_timestamp, i.trust_level
237 FROM identity i LEFT JOIN recipient r ON i.recipient_id = r._id
238 WHERE uuid IS NOT NULL;
240 ALTER TABLE identity2 RENAME TO identity;
242 DROP INDEX msl_recipient_index;
243 ALTER TABLE message_send_log ADD COLUMN uuid BLOB;
244 UPDATE message_send_log
246 FROM message_send_log i, (SELECT _id, uuid FROM recipient) AS r
247 WHERE i.recipient_id = r._id;
248 DELETE FROM message_send_log WHERE uuid IS NULL;
249 ALTER TABLE message_send_log DROP COLUMN recipient_id;
250 CREATE INDEX msl_recipient_index ON message_send_log (uuid, device_id, content_id);
252 CREATE TABLE sender_key2 (
253 _id INTEGER PRIMARY KEY,
255 device_id INTEGER NOT NULL,
256 distribution_id BLOB NOT NULL,
257 record BLOB NOT NULL,
258 created_timestamp INTEGER NOT NULL,
259 UNIQUE(uuid, device_id, distribution_id)
261 INSERT INTO sender_key2 (_id, uuid, device_id, distribution_id, record, created_timestamp)
262 SELECT s._id, r.uuid, s.device_id, s.distribution_id, s.record, s.created_timestamp
263 FROM sender_key s LEFT JOIN recipient r ON s.recipient_id = r._id
264 WHERE uuid IS NOT NULL;
265 DROP TABLE sender_key;
266 ALTER TABLE sender_key2 RENAME TO sender_key;
268 CREATE TABLE sender_key_shared2 (
269 _id INTEGER PRIMARY KEY,
271 device_id INTEGER NOT NULL,
272 distribution_id BLOB NOT NULL,
273 timestamp INTEGER NOT NULL,
274 UNIQUE(uuid, device_id, distribution_id)
276 INSERT INTO sender_key_shared2 (_id, uuid, device_id, distribution_id, timestamp)
277 SELECT s._id, r.uuid, s.device_id, s.distribution_id, s.timestamp
278 FROM sender_key_shared s LEFT JOIN recipient r ON s.recipient_id = r._id
279 WHERE uuid IS NOT NULL;
280 DROP TABLE sender_key_shared;
281 ALTER TABLE sender_key_shared2 RENAME TO sender_key_shared;
283 CREATE TABLE session2 (
284 _id INTEGER PRIMARY KEY,
285 account_id_type INTEGER NOT NULL,
287 device_id INTEGER NOT NULL,
288 record BLOB NOT NULL,
289 UNIQUE(account_id_type, uuid, device_id)
291 INSERT INTO session2 (_id, account_id_type, uuid, device_id, record)
292 SELECT s._id, s.account_id_type, r.uuid, s.device_id, s.record
293 FROM session s LEFT JOIN recipient r ON s.recipient_id = r._id
294 WHERE uuid IS NOT NULL;
296 ALTER TABLE session2 RENAME TO session;
300 if (oldVersion
< 11) {
301 logger
.debug("Updating database: Adding pni field");
302 try (final var statement
= connection
.createStatement()) {
303 statement
.executeUpdate("""
304 ALTER TABLE recipient ADD COLUMN pni BLOB;
308 if (oldVersion
< 12) {
309 logger
.debug("Updating database: Adding username field");
310 try (final var statement
= connection
.createStatement()) {
311 statement
.executeUpdate("""
312 ALTER TABLE recipient ADD COLUMN username TEXT;
316 if (oldVersion
< 13) {
317 logger
.debug("Updating database: Cleanup unknown service ids");
320 DELETE FROM identity AS i
323 try (final var statement
= connection
.prepareStatement(sql
)) {
324 statement
.setBytes(1, ACI
.UNKNOWN
.toByteArray());
325 statement
.executeUpdate();
330 DELETE FROM sender_key_shared AS i
333 try (final var statement
= connection
.prepareStatement(sql
)) {
334 statement
.setBytes(1, ACI
.UNKNOWN
.toByteArray());
335 statement
.executeUpdate();
339 if (oldVersion
< 14) {
340 logger
.debug("Updating database: Creating kyber_pre_key table");
342 try (final var statement
= connection
.createStatement()) {
343 statement
.executeUpdate("""
344 CREATE TABLE kyber_pre_key (
345 _id INTEGER PRIMARY KEY,
346 account_id_type INTEGER NOT NULL,
347 key_id INTEGER NOT NULL,
348 serialized BLOB NOT NULL,
349 is_last_resort INTEGER NOT NULL,
350 UNIQUE(account_id_type, key_id)
356 if (oldVersion
< 15) {
357 logger
.debug("Updating database: Store serviceId as TEXT");
358 try (final var statement
= connection
.createStatement()) {
359 statement
.executeUpdate("""
360 CREATE TABLE tmp_mapping_table (
362 address TEXT NOT NULL
372 final var uuidAddressMapping
= new HashMap
<UUID
, ServiceId
>();
373 try (final var preparedStatement
= connection
.prepareStatement(sql
)) {
374 try (var result
= Utils
.executeQueryForStream(preparedStatement
, (resultSet
) -> {
375 final var pni
= Optional
.ofNullable(resultSet
.getBytes("pni"))
376 .map(UuidUtil
::parseOrNull
)
377 .map(ServiceId
.PNI
::from
);
378 final var serviceIdUuid
= Optional
.ofNullable(resultSet
.getBytes("uuid"))
379 .map(UuidUtil
::parseOrNull
);
380 final var serviceId
= serviceIdUuid
.isPresent() && pni
.isPresent() && serviceIdUuid
.get()
381 .equals(pni
.get().getRawUuid())
382 ? pni
.<ServiceId
>map(p
-> p
)
383 : serviceIdUuid
.<ServiceId
>map(ACI
::from
);
385 return new Pair
<>(serviceId
, pni
);
387 result
.forEach(p
-> {
388 final var serviceId
= p
.first();
389 final var pni
= p
.second();
390 if (serviceId
.isPresent()) {
391 uuidAddressMapping
.put(serviceId
.get().getRawUuid(), serviceId
.get());
393 if (pni
.isPresent()) {
394 uuidAddressMapping
.put(pni
.get().getRawUuid(), pni
.get());
400 final var insertSql
= """
401 INSERT INTO tmp_mapping_table (uuid, address)
404 try (final var insertStatement
= connection
.prepareStatement(insertSql
)) {
405 for (final var entry
: uuidAddressMapping
.entrySet()) {
406 final var uuid
= entry
.getKey();
407 final var serviceId
= entry
.getValue();
408 insertStatement
.setBytes(1, UuidUtil
.toByteArray(uuid
));
409 insertStatement
.setString(2, serviceId
.toString());
410 insertStatement
.execute();
414 statement
.executeUpdate("""
415 CREATE TABLE identity2 (
416 _id INTEGER PRIMARY KEY,
417 address TEXT UNIQUE NOT NULL,
418 identity_key BLOB NOT NULL,
419 added_timestamp INTEGER NOT NULL,
420 trust_level INTEGER NOT NULL
422 INSERT INTO identity2 (_id, address, identity_key, added_timestamp, trust_level)
423 SELECT i._id, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = i.uuid) address, i.identity_key, i.added_timestamp, i.trust_level
425 WHERE address IS NOT NULL;
427 ALTER TABLE identity2 RENAME TO identity;
429 CREATE TABLE message_send_log2 (
430 _id INTEGER PRIMARY KEY,
431 content_id INTEGER NOT NULL REFERENCES message_send_log_content (_id) ON DELETE CASCADE,
432 address TEXT NOT NULL,
433 device_id INTEGER NOT NULL
435 INSERT INTO message_send_log2 (_id, content_id, address, device_id)
436 SELECT m._id, m.content_id, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = m.uuid) address, m.device_id
437 FROM message_send_log m
438 WHERE address IS NOT NULL;
439 DROP INDEX msl_recipient_index;
440 DROP INDEX msl_content_index;
441 DROP TABLE message_send_log;
442 ALTER TABLE message_send_log2 RENAME TO message_send_log;
443 CREATE INDEX msl_recipient_index ON message_send_log (address, device_id, content_id);
444 CREATE INDEX msl_content_index ON message_send_log (content_id);
446 CREATE TABLE sender_key2 (
447 _id INTEGER PRIMARY KEY,
448 address TEXT NOT NULL,
449 device_id INTEGER NOT NULL,
450 distribution_id BLOB NOT NULL,
451 record BLOB NOT NULL,
452 created_timestamp INTEGER NOT NULL,
453 UNIQUE(address, device_id, distribution_id)
455 INSERT INTO sender_key2 (_id, address, device_id, distribution_id, record, created_timestamp)
456 SELECT s._id, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = s.uuid) address, s.device_id, s.distribution_id, s.record, s.created_timestamp
458 WHERE address IS NOT NULL;
459 DROP TABLE sender_key;
460 ALTER TABLE sender_key2 RENAME TO sender_key;
462 CREATE TABLE sender_key_shared2 (
463 _id INTEGER PRIMARY KEY,
464 address TEXT NOT NULL,
465 device_id INTEGER NOT NULL,
466 distribution_id BLOB NOT NULL,
467 timestamp INTEGER NOT NULL,
468 UNIQUE(address, device_id, distribution_id)
470 INSERT INTO sender_key_shared2 (_id, address, device_id, distribution_id, timestamp)
471 SELECT s._id, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = s.uuid) address, s.device_id, s.distribution_id, s.timestamp
472 FROM sender_key_shared s
473 WHERE address IS NOT NULL;
474 DROP TABLE sender_key_shared;
475 ALTER TABLE sender_key_shared2 RENAME TO sender_key_shared;
477 CREATE TABLE session2 (
478 _id INTEGER PRIMARY KEY,
479 account_id_type INTEGER NOT NULL,
480 address TEXT NOT NULL,
481 device_id INTEGER NOT NULL,
482 record BLOB NOT NULL,
483 UNIQUE(account_id_type, address, device_id)
485 INSERT INTO session2 (_id, account_id_type, address, device_id, record)
486 SELECT s._id, s.account_id_type, (SELECT t.address FROM tmp_mapping_table t WHERE t.uuid = s.uuid) address, s.device_id, s.record
488 WHERE address IS NOT NULL;
490 ALTER TABLE session2 RENAME TO session;
492 DROP TABLE tmp_mapping_table;
496 if (oldVersion
< 16) {
497 logger
.debug("Updating database: Adding stale_timestamp prekey field");
498 try (final var statement
= connection
.createStatement()) {
499 statement
.executeUpdate("""
500 ALTER TABLE pre_key ADD COLUMN stale_timestamp INTEGER;
501 ALTER TABLE kyber_pre_key ADD COLUMN stale_timestamp INTEGER;
502 ALTER TABLE kyber_pre_key ADD COLUMN timestamp INTEGER DEFAULT 0;