├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── pages.yml ├── .gitignore ├── .sqlx ├── query-063159b2ecf475363254eaffd8c3286b4354a2f05a1eb418678ffc3050e09e86.json ├── query-0f3b16503d75d03ffae6549b8ed15de08768a88a347555a849ec5d192979355f.json ├── query-14a77634b0af69062c8cc9872304b0f0f67e7d2bff863aaf15acbd679537dee5.json ├── query-1d2edb6c2ba8eb31ad6d233bd30b215223987ea5797b76e0396ed41eddfb26c6.json ├── query-2021e07a71430a7601f24e7a02db25cc447da83e1240fb63274cdf8f336786e1.json ├── query-22079faf4ad4833aa9b9682a8f6a62bf1e0fde6b80aa2b6a80d2e68f37171531.json ├── query-229af3dbe01148ac55574ac7de81f9535210d0165bbd6f0f212242a3a62249e8.json ├── query-23c67d5b7ad86de40a910d3b7680f0b95aa3b4de8b49fa6424af6d219b52280a.json ├── query-2fea0c549ed5dcbb12c057eab578159fee5232b7202594d98b2e770fc2767cb3.json ├── query-3957d3b7ac391d8b3712f71043ea99136fdb4dfe1c8b3d813f5b78559fd1456b.json ├── query-46a3a105384156be962f6825ee84d66e68c4dd10b5745b024ced234599ecb330.json ├── query-512c20eacc7d1ef25eda9bebcdb362a7cd983cb49ea5671c34f7eace39d8d51f.json ├── query-54bfe330f2fe418f03f78d471a42149900b4a154adb466d123fdf9f221af5971.json ├── query-55efa60cb803a733ed1ec29431aae1bbd23b670d6a6f70a10a87d1037bc05d87.json ├── query-5b76d0a4e70c0ca0353359d81fb64f0a13c34eb5a4048a3ebbfe5e4069d15719.json ├── query-5bc0a6578cc589b5f02a97c6441b85858b02d9cb67007d8e4a5accc88d8005ea.json ├── query-5d0c805a6fc33ca97f82941bd0a907e8ffe17395f7721138b5ae9f561b55be93.json ├── query-621de0b2a2451d04de718349f7bddfbdae66d2c8698c62457ca8cfb26539553e.json ├── query-639fbd073012246a7884f5950b990f956c477d4317643f85fc09002104e9cb95.json ├── query-6596b08f27ca4a47ed041e593a9c8a1621abb0336cf79a714a80904c6a85c7ed.json ├── query-67999ceeb5578a62a2f4d03cb202437dda869071494ae0674e8971482ae3e8d1.json ├── query-75843c68da0d7af5503404586bcf6f78a45867816ea459e8cb2cc2bacf66812a.json ├── query-759758c0c79377c3004e2b04ecca0e292524dc7965bab1eea79bd714134eba59.json ├── query-76ad2cb925888727b13530850f194b62ad1f1469252c797266181b0a295dbefa.json ├── query-7b021156864bb30c24b8bc4ed0f1a0eb67f59b09b0132a9e156b31154f8787e6.json ├── query-7d020ddc267ffdc2ae06427a1ce2aff08c98828abf87c5856e5a370eb2eadca7.json ├── query-87c26cf098c55e69ddb0ffa7145fdf7be0820ca492b735c065b665214e9e4b7b.json ├── query-8c1061a398d8e0df5ab4e756a2dc80d7200e485d78371a125d02dfcf264029f8.json ├── query-8ddf5edb9c27f8c4a551e12a7852d6575c1207cb43e19804593a52d37ccb5e77.json ├── query-91190bfed9a18bbd7ca0b90f01e56e2289b567a8269c6e5598873d1d3bafc014.json ├── query-92e6d334e1c9dc7c84054be8deeb14f7ca94450d414940f65308510450c6b416.json ├── query-9325a70c494cad6e4eff76e8ddabbc17fe6f0d496cd3e0f1328fdce83144a547.json ├── query-991269516dba7881b75250cf704f6357f9235de096f3c3019708666dd2b6a460.json ├── query-99df5c24c97704019d0f2a8a146e0db41d4493ffde5cc3ff357974bb0f529f66.json ├── query-a20783a0a52ff731e78f78e041b8328073c3a17a23dd14d2789cde592c36a6af.json ├── query-a273c9f280c542a796ba33c7d28a8dd46afcf57ee09633de84f50911dbd1c68c.json ├── query-a4d6676a22c433425e9892ed1930cdfab4d5c25cb686f27125fdb3902c31dfbd.json ├── query-a556d09e8318318f36724d7ec36b1d692c98293dc0f6cdab31ee842a2f38dcc9.json ├── query-a6639387055b76c0c1c81179abf7f4b536c614ab54afcf2c7374d4ab5784e705.json ├── query-b01ca4e2420035eeb33e86b85f6c893b81393386ec2dcb4fc3a86e8f62065a22.json ├── query-b14d036992c98c6b4924cbe0c9b73ab4699b0de64f110584462e17f740bcbf86.json ├── query-b57fe7e8be13b69eb7791e1e47505d3f2f5a12703cb04a1cde9ceb7f56d0db8a.json ├── query-b8132dfdac9d908be4a0456599e6baaa6295ed358dac091476662ac6b137ee8c.json ├── query-b9b8df9c4968459e1da40a35b6c83e26514ccddd11f319e5b5982af9333bf34d.json ├── query-bb92893b22752f1fe76ca9c514d3430ee7239bb622d7d00a6c504ee9cbbe37da.json ├── query-beff2826440b05111d590d2bccbd71c8de2e35e5008d73fbcde86a55652dd1c6.json ├── query-bf52cac41388a9a6bc0651bd20c7112075c2d74508aaf65da107c5673d006728.json ├── query-bfd4ce8512474871273c20c7e67fbd260b1c1650dba73cd9bd0e35d2bfa4588a.json ├── query-c43853cc74ab0f2f94d0f05231c731102675cd7bf2f7857cc5f2a5ebfcf2a208.json ├── query-c457798448aaf00ffc7f74b348e02d1e35b7fd995e2ba8b9ad28be0d5a49634e.json ├── query-ca42268d6f655f90e617c3bed9acc69921a8b5a83441a7aad3ee3678a63a4242.json ├── query-cb2555708665c60b7f8ed462dec5925a7f01fd12f059755cf8cfc58bdace49c1.json ├── query-d98db13a3ec012ba30a2456f57fb7ddeebe2206372655c5c2cef1c5649237eee.json ├── query-e3493e11d6229ac302bd3d974221f5499d2683906d3d9470939da0c2616a9ce5.json ├── query-e617e730d0cbeb70fdabc4a54e6b5ff696f4720ec649057681553fd60206c624.json ├── query-e945af97e789d44a88f923c8cfd8f2fdcf9810d1df6667e1d5ac55aa9a98d88e.json ├── query-eb7691e6f4227aa79fe925e5f181cff2dca7bf655630d48580e74391c2579a7a.json ├── query-eebeb774c217a2eaa7ec8b8fcf8066ad8efc48f19672d3f293bcbec591aaca49.json ├── query-efd72a989458fced2cdc60b006c43a1464d9db3e141ceaa2978207659b38572a.json ├── query-f0a273c8a87f790e3d0cb7595f2f401f35d0abfa4c6cc6a94972e46eb059ad30.json ├── query-f1db0046d82fe0e9fabd178da73839fc046295e6094775a9186e8a13e0e3eef0.json └── query-fadab658b7f0c0dfd2444950966fd8f7f95032eda602bfd5afbc62d6061981f9.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── justfile ├── presage-cli ├── Cargo.toml └── src │ └── main.rs ├── presage-store-cipher ├── Cargo.toml └── src │ └── lib.rs ├── presage-store-sled ├── Cargo.toml ├── build.rs └── src │ ├── content.rs │ ├── error.rs │ ├── lib.rs │ ├── protobuf.rs │ ├── protobuf │ └── InternalSerialization.proto │ └── protocol.rs ├── presage-store-sqlite ├── Cargo.toml ├── migrations │ └── 20250112201436_init.sql └── src │ ├── content.rs │ ├── data.rs │ ├── error.rs │ ├── lib.rs │ └── protocol.rs ├── presage ├── Cargo.toml └── src │ ├── errors.rs │ ├── lib.rs │ ├── manager │ ├── confirmation.rs │ ├── linking.rs │ ├── mod.rs │ ├── registered.rs │ └── registration.rs │ ├── model │ ├── contacts.rs │ ├── groups.rs │ ├── identity.rs │ ├── messages.rs │ └── mod.rs │ ├── serde.rs │ └── store.rs └── rustfmt.toml /.gitattributes: -------------------------------------------------------------------------------- 1 | .sqlx/* -diff -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - v0.* 8 | pull_request: 9 | 10 | env: 11 | CARGO_INCREMENTAL: 0 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build_and_test: 19 | name: cargo ${{ matrix.cargo_flags }} 20 | runs-on: ubuntu-latest 21 | env: 22 | RUSTFLAGS: -D warnings 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Rust toolchain 28 | uses: dtolnay/rust-toolchain@v1 29 | with: 30 | toolchain: stable 31 | 32 | - name: Install protobuf 33 | run: | 34 | sudo apt-get update 35 | sudo apt-get install -y protobuf-compiler 36 | 37 | - name: Configure CI cache 38 | uses: Swatinem/rust-cache@v2 39 | 40 | - name: Build 41 | run: cargo build --all-targets 42 | 43 | - name: Test 44 | run: cargo test --all-targets 45 | 46 | - name: Test docs 47 | run: cargo test --doc 48 | 49 | rustfmt: 50 | name: rustfmt 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout repository 54 | uses: actions/checkout@v4 55 | 56 | - name: Setup Rust toolchain 57 | uses: dtolnay/rust-toolchain@v1 58 | with: 59 | toolchain: stable 60 | components: rustfmt 61 | 62 | - name: Check code format 63 | run: cargo fmt -- --check 64 | 65 | clippy: 66 | name: clippy 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Checkout repository 70 | uses: actions/checkout@v4 71 | 72 | - name: Setup Rust toolchain 73 | uses: dtolnay/rust-toolchain@v1 74 | with: 75 | toolchain: stable 76 | components: clippy 77 | 78 | - name: Install protobuf 79 | run: | 80 | sudo apt-get update 81 | sudo apt-get install -y protobuf-compiler 82 | 83 | - name: Setup CI cache 84 | uses: Swatinem/rust-cache@v2 85 | 86 | - name: Run clippy lints 87 | run: cargo clippy 88 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: GitHub Pages 7 | 8 | jobs: 9 | pages: 10 | name: Update 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false 16 | - name: Install protobuf 17 | run: | 18 | sudo apt-get update 19 | sudo apt-get install -y protobuf-compiler 20 | - name: Setup Rust toolchain 21 | uses: dtolnay/rust-toolchain@v1 22 | with: 23 | toolchain: stable 24 | - run: cargo doc --no-deps -p presage -p libsignal-service -p libsignal-protocol -p zkgroup 25 | - uses: JamesIves/github-pages-deploy-action@3.7.1 26 | with: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | BRANCH: gh-pages 29 | FOLDER: target/doc 30 | CLEAN: true # Automatically remove deleted files from the deploy branch 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # Test sqlite database 9 | /test.db 10 | 11 | # VSCode debugger config 12 | .vscode/launch.json 13 | -------------------------------------------------------------------------------- /.sqlx/query-063159b2ecf475363254eaffd8c3286b4354a2f05a1eb418678ffc3050e09e86.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO contacts\n VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 10 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "063159b2ecf475363254eaffd8c3286b4354a2f05a1eb418678ffc3050e09e86" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-0f3b16503d75d03ffae6549b8ed15de08768a88a347555a849ec5d192979355f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT key FROM profile_keys WHERE uuid = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "key", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "0f3b16503d75d03ffae6549b8ed15de08768a88a347555a849ec5d192979355f" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-14a77634b0af69062c8cc9872304b0f0f67e7d2bff863aaf15acbd679537dee5.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO pre_keys (id, identity, record)\n VALUES (?1, ?2, ?3)\n ON CONFLICT DO UPDATE SET record = ?3", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 3 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "14a77634b0af69062c8cc9872304b0f0f67e7d2bff863aaf15acbd679537dee5" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-1d2edb6c2ba8eb31ad6d233bd30b215223987ea5797b76e0396ed41eddfb26c6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO kyber_pre_keys (id, identity, record)\n VALUES (?1, ?2, ?3)\n ON CONFLICT DO UPDATE SET record = ?3", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 3 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "1d2edb6c2ba8eb31ad6d233bd30b215223987ea5797b76e0396ed41eddfb26c6" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-2021e07a71430a7601f24e7a02db25cc447da83e1240fb63274cdf8f336786e1.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT record FROM sessions\n WHERE address = ? AND device_id = ? AND identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "record", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 3 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "2021e07a71430a7601f24e7a02db25cc447da83e1240fb63274cdf8f336786e1" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-22079faf4ad4833aa9b9682a8f6a62bf1e0fde6b80aa2b6a80d2e68f37171531.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO threads(recipient_id, group_master_key) VALUES (?1, NULL)\n ON CONFLICT DO UPDATE SET recipient_id = ?1 RETURNING id", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "22079faf4ad4833aa9b9682a8f6a62bf1e0fde6b80aa2b6a80d2e68f37171531" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-229af3dbe01148ac55574ac7de81f9535210d0165bbd6f0f212242a3a62249e8.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT value FROM kv WHERE key = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "value", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "229af3dbe01148ac55574ac7de81f9535210d0165bbd6f0f212242a3a62249e8" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-23c67d5b7ad86de40a910d3b7680f0b95aa3b4de8b49fa6424af6d219b52280a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT\n master_key,\n title,\n revision AS \"revision: _\",\n invite_link_password,\n access_control AS \"access_control: _\",\n avatar,\n description,\n members AS \"members: _\",\n pending_members AS \"pending_members: _\",\n requesting_members AS \"requesting_members: _\"\n FROM groups\n WHERE master_key = ?\n LIMIT 1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "master_key", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | }, 11 | { 12 | "name": "title", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | }, 16 | { 17 | "name": "revision: _", 18 | "ordinal": 2, 19 | "type_info": "Integer" 20 | }, 21 | { 22 | "name": "invite_link_password", 23 | "ordinal": 3, 24 | "type_info": "Blob" 25 | }, 26 | { 27 | "name": "access_control: _", 28 | "ordinal": 4, 29 | "type_info": "Blob" 30 | }, 31 | { 32 | "name": "avatar", 33 | "ordinal": 5, 34 | "type_info": "Text" 35 | }, 36 | { 37 | "name": "description", 38 | "ordinal": 6, 39 | "type_info": "Text" 40 | }, 41 | { 42 | "name": "members: _", 43 | "ordinal": 7, 44 | "type_info": "Blob" 45 | }, 46 | { 47 | "name": "pending_members: _", 48 | "ordinal": 8, 49 | "type_info": "Blob" 50 | }, 51 | { 52 | "name": "requesting_members: _", 53 | "ordinal": 9, 54 | "type_info": "Blob" 55 | } 56 | ], 57 | "parameters": { 58 | "Right": 1 59 | }, 60 | "nullable": [ 61 | false, 62 | false, 63 | false, 64 | true, 65 | true, 66 | false, 67 | true, 68 | false, 69 | false, 70 | false 71 | ] 72 | }, 73 | "hash": "23c67d5b7ad86de40a910d3b7680f0b95aa3b4de8b49fa6424af6d219b52280a" 74 | } 75 | -------------------------------------------------------------------------------- /.sqlx/query-2fea0c549ed5dcbb12c057eab578159fee5232b7202594d98b2e770fc2767cb3.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT\n ts AS \"ts: _\",\n sender_service_id,\n sender_device_id AS \"sender_device_id: _\",\n destination_service_id,\n needs_receipt,\n unidentified_sender,\n content_body,\n was_plaintext\n FROM thread_messages\n WHERE thread_id = (\n SELECT id FROM threads WHERE group_master_key = ? OR recipient_id = ?)\n AND coalesce(ts > ?, ts >= ?, true)\n AND coalesce(ts < ?, ts <= ?, true)\n ORDER BY ts DESC", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "ts: _", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | }, 11 | { 12 | "name": "sender_service_id", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | }, 16 | { 17 | "name": "sender_device_id: _", 18 | "ordinal": 2, 19 | "type_info": "Integer" 20 | }, 21 | { 22 | "name": "destination_service_id", 23 | "ordinal": 3, 24 | "type_info": "Text" 25 | }, 26 | { 27 | "name": "needs_receipt", 28 | "ordinal": 4, 29 | "type_info": "Bool" 30 | }, 31 | { 32 | "name": "unidentified_sender", 33 | "ordinal": 5, 34 | "type_info": "Bool" 35 | }, 36 | { 37 | "name": "content_body", 38 | "ordinal": 6, 39 | "type_info": "Blob" 40 | }, 41 | { 42 | "name": "was_plaintext", 43 | "ordinal": 7, 44 | "type_info": "Bool" 45 | } 46 | ], 47 | "parameters": { 48 | "Right": 6 49 | }, 50 | "nullable": [ 51 | false, 52 | false, 53 | false, 54 | false, 55 | false, 56 | false, 57 | false, 58 | false 59 | ] 60 | }, 61 | "hash": "2fea0c549ed5dcbb12c057eab578159fee5232b7202594d98b2e770fc2767cb3" 62 | } 63 | -------------------------------------------------------------------------------- /.sqlx/query-3957d3b7ac391d8b3712f71043ea99136fdb4dfe1c8b3d813f5b78559fd1456b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT\n master_key,\n title,\n revision AS \"revision: _\",\n invite_link_password,\n access_control AS \"access_control: _\",\n avatar,\n description,\n members AS \"members: _\",\n pending_members AS \"pending_members: _\",\n requesting_members AS \"requesting_members: _\"\n FROM groups", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "master_key", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | }, 11 | { 12 | "name": "title", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | }, 16 | { 17 | "name": "revision: _", 18 | "ordinal": 2, 19 | "type_info": "Integer" 20 | }, 21 | { 22 | "name": "invite_link_password", 23 | "ordinal": 3, 24 | "type_info": "Blob" 25 | }, 26 | { 27 | "name": "access_control: _", 28 | "ordinal": 4, 29 | "type_info": "Blob" 30 | }, 31 | { 32 | "name": "avatar", 33 | "ordinal": 5, 34 | "type_info": "Text" 35 | }, 36 | { 37 | "name": "description", 38 | "ordinal": 6, 39 | "type_info": "Text" 40 | }, 41 | { 42 | "name": "members: _", 43 | "ordinal": 7, 44 | "type_info": "Blob" 45 | }, 46 | { 47 | "name": "pending_members: _", 48 | "ordinal": 8, 49 | "type_info": "Blob" 50 | }, 51 | { 52 | "name": "requesting_members: _", 53 | "ordinal": 9, 54 | "type_info": "Blob" 55 | } 56 | ], 57 | "parameters": { 58 | "Right": 0 59 | }, 60 | "nullable": [ 61 | false, 62 | false, 63 | false, 64 | true, 65 | true, 66 | false, 67 | true, 68 | false, 69 | false, 70 | false 71 | ] 72 | }, 73 | "hash": "3957d3b7ac391d8b3712f71043ea99136fdb4dfe1c8b3d813f5b78559fd1456b" 74 | } 75 | -------------------------------------------------------------------------------- /.sqlx/query-46a3a105384156be962f6825ee84d66e68c4dd10b5745b024ced234599ecb330.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM thread_messages\n WHERE ts = ? AND thread_id = (\n SELECT id FROM threads WHERE group_master_key = ? OR recipient_id = ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 3 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "46a3a105384156be962f6825ee84d66e68c4dd10b5745b024ced234599ecb330" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-512c20eacc7d1ef25eda9bebcdb362a7cd983cb49ea5671c34f7eace39d8d51f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT\n uuid AS \"uuid: _\",\n phone_number,\n name,\n color,\n profile_key,\n expire_timer,\n expire_timer_version,\n inbox_position,\n archived,\n avatar,\n destination_aci AS \"destination_aci: _\",\n identity_key,\n is_verified\n FROM contacts c\n LEFT JOIN contacts_verification_state cv ON c.uuid = cv.destination_aci\n ORDER BY c.inbox_position", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "uuid: _", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | }, 11 | { 12 | "name": "phone_number", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | }, 16 | { 17 | "name": "name", 18 | "ordinal": 2, 19 | "type_info": "Text" 20 | }, 21 | { 22 | "name": "color", 23 | "ordinal": 3, 24 | "type_info": "Text" 25 | }, 26 | { 27 | "name": "profile_key", 28 | "ordinal": 4, 29 | "type_info": "Blob" 30 | }, 31 | { 32 | "name": "expire_timer", 33 | "ordinal": 5, 34 | "type_info": "Integer" 35 | }, 36 | { 37 | "name": "expire_timer_version", 38 | "ordinal": 6, 39 | "type_info": "Integer" 40 | }, 41 | { 42 | "name": "inbox_position", 43 | "ordinal": 7, 44 | "type_info": "Integer" 45 | }, 46 | { 47 | "name": "archived", 48 | "ordinal": 8, 49 | "type_info": "Bool" 50 | }, 51 | { 52 | "name": "avatar", 53 | "ordinal": 9, 54 | "type_info": "Blob" 55 | }, 56 | { 57 | "name": "destination_aci: _", 58 | "ordinal": 10, 59 | "type_info": "Blob" 60 | }, 61 | { 62 | "name": "identity_key", 63 | "ordinal": 11, 64 | "type_info": "Blob" 65 | }, 66 | { 67 | "name": "is_verified", 68 | "ordinal": 12, 69 | "type_info": "Bool" 70 | } 71 | ], 72 | "parameters": { 73 | "Right": 0 74 | }, 75 | "nullable": [ 76 | false, 77 | true, 78 | false, 79 | true, 80 | false, 81 | false, 82 | false, 83 | false, 84 | false, 85 | true, 86 | true, 87 | true, 88 | true 89 | ] 90 | }, 91 | "hash": "512c20eacc7d1ef25eda9bebcdb362a7cd983cb49ea5671c34f7eace39d8d51f" 92 | } 93 | -------------------------------------------------------------------------------- /.sqlx/query-54bfe330f2fe418f03f78d471a42149900b4a154adb466d123fdf9f221af5971.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM sessions WHERE address = ? AND device_id = ? AND identity = ?", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 3 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "54bfe330f2fe418f03f78d471a42149900b4a154adb466d123fdf9f221af5971" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-55efa60cb803a733ed1ec29431aae1bbd23b670d6a6f70a10a87d1037bc05d87.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT value FROM kv WHERE key = 'registration'", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "value", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 0 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "55efa60cb803a733ed1ec29431aae1bbd23b670d6a6f70a10a87d1037bc05d87" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-5b76d0a4e70c0ca0353359d81fb64f0a13c34eb5a4048a3ebbfe5e4069d15719.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM contacts", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 0 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "5b76d0a4e70c0ca0353359d81fb64f0a13c34eb5a4048a3ebbfe5e4069d15719" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-5bc0a6578cc589b5f02a97c6441b85858b02d9cb67007d8e4a5accc88d8005ea.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO profiles VALUES (?, ?, ?, ?, ?, ?, ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 7 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "5bc0a6578cc589b5f02a97c6441b85858b02d9cb67007d8e4a5accc88d8005ea" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-5d0c805a6fc33ca97f82941bd0a907e8ffe17395f7721138b5ae9f561b55be93.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM kv WHERE key = 'registration'", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 0 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "5d0c805a6fc33ca97f82941bd0a907e8ffe17395f7721138b5ae9f561b55be93" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-621de0b2a2451d04de718349f7bddfbdae66d2c8698c62457ca8cfb26539553e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM kv", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 0 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "621de0b2a2451d04de718349f7bddfbdae66d2c8698c62457ca8cfb26539553e" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-639fbd073012246a7884f5950b990f956c477d4317643f85fc09002104e9cb95.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT device_id AS 'id: u32' FROM sessions\n WHERE address = ? AND device_id != ? AND identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id: u32", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 3 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "639fbd073012246a7884f5950b990f956c477d4317643f85fc09002104e9cb95" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-6596b08f27ca4a47ed041e593a9c8a1621abb0336cf79a714a80904c6a85c7ed.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT COUNT(id) FROM signed_pre_keys WHERE identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "COUNT(id)", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "6596b08f27ca4a47ed041e593a9c8a1621abb0336cf79a714a80904c6a85c7ed" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-67999ceeb5578a62a2f4d03cb202437dda869071494ae0674e8971482ae3e8d1.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO profile_keys (uuid, key) VALUES (?, ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "67999ceeb5578a62a2f4d03cb202437dda869071494ae0674e8971482ae3e8d1" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-75843c68da0d7af5503404586bcf6f78a45867816ea459e8cb2cc2bacf66812a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM groups", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 0 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "75843c68da0d7af5503404586bcf6f78a45867816ea459e8cb2cc2bacf66812a" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-759758c0c79377c3004e2b04ecca0e292524dc7965bab1eea79bd714134eba59.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT COUNT(id) FROM kyber_pre_keys WHERE identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "COUNT(id)", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "759758c0c79377c3004e2b04ecca0e292524dc7965bab1eea79bd714134eba59" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-76ad2cb925888727b13530850f194b62ad1f1469252c797266181b0a295dbefa.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "UPDATE sessions SET record = ?4\n WHERE address = ?1 AND device_id = ?2 AND identity = ?3", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 4 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "76ad2cb925888727b13530850f194b62ad1f1469252c797266181b0a295dbefa" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-7b021156864bb30c24b8bc4ed0f1a0eb67f59b09b0132a9e156b31154f8787e6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO signed_pre_keys (id, identity, record)\n VALUES (?1, ?2, ?3)\n ON CONFLICT DO UPDATE SET record = ?3", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 3 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "7b021156864bb30c24b8bc4ed0f1a0eb67f59b09b0132a9e156b31154f8787e6" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-7d020ddc267ffdc2ae06427a1ce2aff08c98828abf87c5856e5a370eb2eadca7.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT record FROM signed_pre_keys WHERE id = ? AND identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "record", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 2 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "7d020ddc267ffdc2ae06427a1ce2aff08c98828abf87c5856e5a370eb2eadca7" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-87c26cf098c55e69ddb0ffa7145fdf7be0820ca492b735c065b665214e9e4b7b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT record FROM identities\n WHERE address = ? AND device_id = ? AND identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "record", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 3 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "87c26cf098c55e69ddb0ffa7145fdf7be0820ca492b735c065b665214e9e4b7b" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-8c1061a398d8e0df5ab4e756a2dc80d7200e485d78371a125d02dfcf264029f8.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM kyber_pre_keys WHERE id = ? AND identity = ?", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "8c1061a398d8e0df5ab4e756a2dc80d7200e485d78371a125d02dfcf264029f8" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-8ddf5edb9c27f8c4a551e12a7852d6575c1207cb43e19804593a52d37ccb5e77.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM sticker_packs WHERE id = ?", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 1 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "8ddf5edb9c27f8c4a551e12a7852d6575c1207cb43e19804593a52d37ccb5e77" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-91190bfed9a18bbd7ca0b90f01e56e2289b567a8269c6e5598873d1d3bafc014.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO thread_messages (\n ts,\n thread_id,\n sender_service_id,\n sender_device_id,\n destination_service_id,\n needs_receipt,\n unidentified_sender,\n content_body,\n was_plaintext\n )\n VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 9 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "91190bfed9a18bbd7ca0b90f01e56e2289b567a8269c6e5598873d1d3bafc014" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-92e6d334e1c9dc7c84054be8deeb14f7ca94450d414940f65308510450c6b416.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT bytes FROM group_avatars WHERE group_master_key = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "bytes", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "92e6d334e1c9dc7c84054be8deeb14f7ca94450d414940f65308510450c6b416" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-9325a70c494cad6e4eff76e8ddabbc17fe6f0d496cd3e0f1328fdce83144a547.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO kv (key, value) VALUES ('sender_certificate', ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 1 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "9325a70c494cad6e4eff76e8ddabbc17fe6f0d496cd3e0f1328fdce83144a547" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-991269516dba7881b75250cf704f6357f9235de096f3c3019708666dd2b6a460.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO groups VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 10 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "991269516dba7881b75250cf704f6357f9235de096f3c3019708666dd2b6a460" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-99df5c24c97704019d0f2a8a146e0db41d4493ffde5cc3ff357974bb0f529f66.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT record FROM kyber_pre_keys\n WHERE identity = ? AND is_last_resort = TRUE", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "record", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "99df5c24c97704019d0f2a8a146e0db41d4493ffde5cc3ff357974bb0f529f66" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-a20783a0a52ff731e78f78e041b8328073c3a17a23dd14d2789cde592c36a6af.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT\n uuid AS \"uuid: _\",\n phone_number,\n name,\n color,\n profile_key,\n expire_timer,\n expire_timer_version,\n inbox_position,\n archived,\n avatar,\n destination_aci AS \"destination_aci: _\",\n identity_key,\n is_verified\n FROM contacts c\n LEFT JOIN contacts_verification_state cv ON c.uuid = cv.destination_aci\n WHERE c.uuid = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "uuid: _", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | }, 11 | { 12 | "name": "phone_number", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | }, 16 | { 17 | "name": "name", 18 | "ordinal": 2, 19 | "type_info": "Text" 20 | }, 21 | { 22 | "name": "color", 23 | "ordinal": 3, 24 | "type_info": "Text" 25 | }, 26 | { 27 | "name": "profile_key", 28 | "ordinal": 4, 29 | "type_info": "Blob" 30 | }, 31 | { 32 | "name": "expire_timer", 33 | "ordinal": 5, 34 | "type_info": "Integer" 35 | }, 36 | { 37 | "name": "expire_timer_version", 38 | "ordinal": 6, 39 | "type_info": "Integer" 40 | }, 41 | { 42 | "name": "inbox_position", 43 | "ordinal": 7, 44 | "type_info": "Integer" 45 | }, 46 | { 47 | "name": "archived", 48 | "ordinal": 8, 49 | "type_info": "Bool" 50 | }, 51 | { 52 | "name": "avatar", 53 | "ordinal": 9, 54 | "type_info": "Blob" 55 | }, 56 | { 57 | "name": "destination_aci: _", 58 | "ordinal": 10, 59 | "type_info": "Blob" 60 | }, 61 | { 62 | "name": "identity_key", 63 | "ordinal": 11, 64 | "type_info": "Blob" 65 | }, 66 | { 67 | "name": "is_verified", 68 | "ordinal": 12, 69 | "type_info": "Bool" 70 | } 71 | ], 72 | "parameters": { 73 | "Right": 1 74 | }, 75 | "nullable": [ 76 | false, 77 | true, 78 | false, 79 | true, 80 | false, 81 | false, 82 | false, 83 | false, 84 | false, 85 | true, 86 | false, 87 | false, 88 | true 89 | ] 90 | }, 91 | "hash": "a20783a0a52ff731e78f78e041b8328073c3a17a23dd14d2789cde592c36a6af" 92 | } 93 | -------------------------------------------------------------------------------- /.sqlx/query-a273c9f280c542a796ba33c7d28a8dd46afcf57ee09633de84f50911dbd1c68c.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM pre_keys WHERE id = ? AND identity = ?", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "a273c9f280c542a796ba33c7d28a8dd46afcf57ee09633de84f50911dbd1c68c" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-a4d6676a22c433425e9892ed1930cdfab4d5c25cb686f27125fdb3902c31dfbd.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO kyber_pre_keys\n (id, identity, is_last_resort, record)\n VALUES (?1, ?2, TRUE, ?3)\n ON CONFLICT DO UPDATE SET is_last_resort = TRUE, record = ?3", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 3 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "a4d6676a22c433425e9892ed1930cdfab4d5c25cb686f27125fdb3902c31dfbd" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-a556d09e8318318f36724d7ec36b1d692c98293dc0f6cdab31ee842a2f38dcc9.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT id, key, manifest AS \"manifest: _\" FROM sticker_packs", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | }, 11 | { 12 | "name": "key", 13 | "ordinal": 1, 14 | "type_info": "Blob" 15 | }, 16 | { 17 | "name": "manifest: _", 18 | "ordinal": 2, 19 | "type_info": "Blob" 20 | } 21 | ], 22 | "parameters": { 23 | "Right": 0 24 | }, 25 | "nullable": [ 26 | false, 27 | false, 28 | false 29 | ] 30 | }, 31 | "hash": "a556d09e8318318f36724d7ec36b1d692c98293dc0f6cdab31ee842a2f38dcc9" 32 | } 33 | -------------------------------------------------------------------------------- /.sqlx/query-a6639387055b76c0c1c81179abf7f4b536c614ab54afcf2c7374d4ab5784e705.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO sender_keys\n (address, device_id, identity, distribution_id, record)\n VALUES (?1, ?2, ?3, ?4, ?5)\n ON CONFLICT DO UPDATE SET record = ?5", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 5 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "a6639387055b76c0c1c81179abf7f4b536c614ab54afcf2c7374d4ab5784e705" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-b01ca4e2420035eeb33e86b85f6c893b81393386ec2dcb4fc3a86e8f62065a22.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT id, key, manifest AS \"manifest: _\" FROM sticker_packs WHERE id = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | }, 11 | { 12 | "name": "key", 13 | "ordinal": 1, 14 | "type_info": "Blob" 15 | }, 16 | { 17 | "name": "manifest: _", 18 | "ordinal": 2, 19 | "type_info": "Blob" 20 | } 21 | ], 22 | "parameters": { 23 | "Right": 1 24 | }, 25 | "nullable": [ 26 | false, 27 | false, 28 | false 29 | ] 30 | }, 31 | "hash": "b01ca4e2420035eeb33e86b85f6c893b81393386ec2dcb4fc3a86e8f62065a22" 32 | } 33 | -------------------------------------------------------------------------------- /.sqlx/query-b14d036992c98c6b4924cbe0c9b73ab4699b0de64f110584462e17f740bcbf86.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT record FROM sender_keys\n WHERE address = ? AND device_id = ? AND identity = ? AND distribution_id = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "record", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 4 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "b14d036992c98c6b4924cbe0c9b73ab4699b0de64f110584462e17f740bcbf86" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-b57fe7e8be13b69eb7791e1e47505d3f2f5a12703cb04a1cde9ceb7f56d0db8a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO sessions (address, device_id, identity, record)\n VALUES (?1, ?2, ?3, ?4)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 4 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "b57fe7e8be13b69eb7791e1e47505d3f2f5a12703cb04a1cde9ceb7f56d0db8a" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-b8132dfdac9d908be4a0456599e6baaa6295ed358dac091476662ac6b137ee8c.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "UPDATE group_avatars SET bytes = ? WHERE group_master_key = ?", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "b8132dfdac9d908be4a0456599e6baaa6295ed358dac091476662ac6b137ee8c" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-b9b8df9c4968459e1da40a35b6c83e26514ccddd11f319e5b5982af9333bf34d.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT\n p.given_name,\n p.family_name,\n p.about,\n p.about_emoji,\n p.avatar,\n p.unrestricted_unidentified_access\n FROM profile_keys pk\n INNER JOIN profiles p ON p.uuid = pk.uuid\n WHERE pk.uuid = ? AND pk.key = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "given_name", 8 | "ordinal": 0, 9 | "type_info": "Text" 10 | }, 11 | { 12 | "name": "family_name", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | }, 16 | { 17 | "name": "about", 18 | "ordinal": 2, 19 | "type_info": "Text" 20 | }, 21 | { 22 | "name": "about_emoji", 23 | "ordinal": 3, 24 | "type_info": "Text" 25 | }, 26 | { 27 | "name": "avatar", 28 | "ordinal": 4, 29 | "type_info": "Text" 30 | }, 31 | { 32 | "name": "unrestricted_unidentified_access", 33 | "ordinal": 5, 34 | "type_info": "Bool" 35 | } 36 | ], 37 | "parameters": { 38 | "Right": 2 39 | }, 40 | "nullable": [ 41 | true, 42 | true, 43 | true, 44 | true, 45 | true, 46 | false 47 | ] 48 | }, 49 | "hash": "b9b8df9c4968459e1da40a35b6c83e26514ccddd11f319e5b5982af9333bf34d" 50 | } 51 | -------------------------------------------------------------------------------- /.sqlx/query-bb92893b22752f1fe76ca9c514d3430ee7239bb622d7d00a6c504ee9cbbe37da.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "bb92893b22752f1fe76ca9c514d3430ee7239bb622d7d00a6c504ee9cbbe37da" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-beff2826440b05111d590d2bccbd71c8de2e35e5008d73fbcde86a55652dd1c6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT bytes FROM profile_avatars WHERE uuid = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "bytes", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "beff2826440b05111d590d2bccbd71c8de2e35e5008d73fbcde86a55652dd1c6" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-bf52cac41388a9a6bc0651bd20c7112075c2d74508aaf65da107c5673d006728.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT record FROM pre_keys WHERE id = ? AND identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "record", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 2 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "bf52cac41388a9a6bc0651bd20c7112075c2d74508aaf65da107c5673d006728" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-bfd4ce8512474871273c20c7e67fbd260b1c1650dba73cd9bd0e35d2bfa4588a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "UPDATE identities SET record = ?4\n WHERE address = ?1 AND device_id = ?2 AND identity = ?3", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 4 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "bfd4ce8512474871273c20c7e67fbd260b1c1650dba73cd9bd0e35d2bfa4588a" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-c43853cc74ab0f2f94d0f05231c731102675cd7bf2f7857cc5f2a5ebfcf2a208.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "UPDATE profile_avatars SET bytes = ? WHERE uuid = ?", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "c43853cc74ab0f2f94d0f05231c731102675cd7bf2f7857cc5f2a5ebfcf2a208" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-c457798448aaf00ffc7f74b348e02d1e35b7fd995e2ba8b9ad28be0d5a49634e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO contacts_verification_state(\n destination_aci, identity_key, is_verified\n ) VALUES(?, ?, ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 3 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "c457798448aaf00ffc7f74b348e02d1e35b7fd995e2ba8b9ad28be0d5a49634e" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-ca42268d6f655f90e617c3bed9acc69921a8b5a83441a7aad3ee3678a63a4242.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM thread_messages WHERE thread_id = (\n SELECT id FROM threads WHERE group_master_key = ? OR recipient_id = ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "ca42268d6f655f90e617c3bed9acc69921a8b5a83441a7aad3ee3678a63a4242" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-cb2555708665c60b7f8ed462dec5925a7f01fd12f059755cf8cfc58bdace49c1.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT MAX(id) AS 'id: u32' FROM pre_keys WHERE identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id: u32", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | true 17 | ] 18 | }, 19 | "hash": "cb2555708665c60b7f8ed462dec5925a7f01fd12f059755cf8cfc58bdace49c1" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-d98db13a3ec012ba30a2456f57fb7ddeebe2206372655c5c2cef1c5649237eee.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO sticker_packs(id, key, manifest) VALUES(?, ?, ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 3 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "d98db13a3ec012ba30a2456f57fb7ddeebe2206372655c5c2cef1c5649237eee" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-e3493e11d6229ac302bd3d974221f5499d2683906d3d9470939da0c2616a9ce5.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO threads(recipient_id, group_master_key) VALUES (NULL, ?1)\n ON CONFLICT DO UPDATE SET group_master_key = ?1 RETURNING id", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "e3493e11d6229ac302bd3d974221f5499d2683906d3d9470939da0c2616a9ce5" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-e617e730d0cbeb70fdabc4a54e6b5ff696f4720ec649057681553fd60206c624.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "DELETE FROM sessions WHERE address = ? AND identity = ?", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 2 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "e617e730d0cbeb70fdabc4a54e6b5ff696f4720ec649057681553fd60206c624" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-e945af97e789d44a88f923c8cfd8f2fdcf9810d1df6667e1d5ac55aa9a98d88e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT\n ts AS \"ts: _\",\n sender_service_id,\n sender_device_id AS \"sender_device_id: _\",\n destination_service_id,\n needs_receipt,\n unidentified_sender,\n content_body,\n was_plaintext\n FROM thread_messages\n WHERE ts = ? AND thread_id = (\n SELECT id FROM threads WHERE group_master_key = ? OR recipient_id = ?)", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "ts: _", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | }, 11 | { 12 | "name": "sender_service_id", 13 | "ordinal": 1, 14 | "type_info": "Text" 15 | }, 16 | { 17 | "name": "sender_device_id: _", 18 | "ordinal": 2, 19 | "type_info": "Integer" 20 | }, 21 | { 22 | "name": "destination_service_id", 23 | "ordinal": 3, 24 | "type_info": "Text" 25 | }, 26 | { 27 | "name": "needs_receipt", 28 | "ordinal": 4, 29 | "type_info": "Bool" 30 | }, 31 | { 32 | "name": "unidentified_sender", 33 | "ordinal": 5, 34 | "type_info": "Bool" 35 | }, 36 | { 37 | "name": "content_body", 38 | "ordinal": 6, 39 | "type_info": "Blob" 40 | }, 41 | { 42 | "name": "was_plaintext", 43 | "ordinal": 7, 44 | "type_info": "Bool" 45 | } 46 | ], 47 | "parameters": { 48 | "Right": 3 49 | }, 50 | "nullable": [ 51 | false, 52 | false, 53 | false, 54 | false, 55 | false, 56 | false, 57 | false, 58 | false 59 | ] 60 | }, 61 | "hash": "e945af97e789d44a88f923c8cfd8f2fdcf9810d1df6667e1d5ac55aa9a98d88e" 62 | } 63 | -------------------------------------------------------------------------------- /.sqlx/query-eb7691e6f4227aa79fe925e5f181cff2dca7bf655630d48580e74391c2579a7a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT MAX(id) AS 'id: u32' FROM kyber_pre_keys WHERE identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id: u32", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | true 17 | ] 18 | }, 19 | "hash": "eb7691e6f4227aa79fe925e5f181cff2dca7bf655630d48580e74391c2579a7a" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-eebeb774c217a2eaa7ec8b8fcf8066ad8efc48f19672d3f293bcbec591aaca49.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT INTO identities (address, device_id, identity, record)\n VALUES (?1, ?2, ?3, ?4)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 4 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "eebeb774c217a2eaa7ec8b8fcf8066ad8efc48f19672d3f293bcbec591aaca49" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-efd72a989458fced2cdc60b006c43a1464d9db3e141ceaa2978207659b38572a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "INSERT OR REPLACE INTO kv (key, value) VALUES ('registration', ?)", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Right": 1 8 | }, 9 | "nullable": [] 10 | }, 11 | "hash": "efd72a989458fced2cdc60b006c43a1464d9db3e141ceaa2978207659b38572a" 12 | } 13 | -------------------------------------------------------------------------------- /.sqlx/query-f0a273c8a87f790e3d0cb7595f2f401f35d0abfa4c6cc6a94972e46eb059ad30.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT MAX(id) AS 'id: u32' FROM signed_pre_keys WHERE identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "id: u32", 8 | "ordinal": 0, 9 | "type_info": "Integer" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 1 14 | }, 15 | "nullable": [ 16 | true 17 | ] 18 | }, 19 | "hash": "f0a273c8a87f790e3d0cb7595f2f401f35d0abfa4c6cc6a94972e46eb059ad30" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-f1db0046d82fe0e9fabd178da73839fc046295e6094775a9186e8a13e0e3eef0.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT value FROM kv WHERE key = 'sender_certificate' LIMIT 1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "value", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 0 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "f1db0046d82fe0e9fabd178da73839fc046295e6094775a9186e8a13e0e3eef0" 20 | } 21 | -------------------------------------------------------------------------------- /.sqlx/query-fadab658b7f0c0dfd2444950966fd8f7f95032eda602bfd5afbc62d6061981f9.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "SQLite", 3 | "query": "SELECT record FROM kyber_pre_keys WHERE id = ? AND identity = ?", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "name": "record", 8 | "ordinal": 0, 9 | "type_info": "Blob" 10 | } 11 | ], 12 | "parameters": { 13 | "Right": 2 14 | }, 15 | "nullable": [ 16 | false 17 | ] 18 | }, 19 | "hash": "fadab658b7f0c0dfd2444950966fd8f7f95032eda602bfd5afbc62d6061981f9" 20 | } 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | ### Fixed 13 | 14 | ### Changed 15 | 16 | ## [0.6.1] 17 | 18 | ### Added 19 | 20 | - Add support for post-quantum session keys (#179) 21 | - Add `Manager::clear_thread` to erase a conversation thread (#157) 22 | - Add `Manager::thread_title` to get the title of a conversation thread (1-1, group, etc.) (#157) 23 | - Add `ReceivingMode` to `Manager::receive_messages` to be able to run an initial sync like Signal-Desktop (#202) 24 | - Verify sha256 checksum after downloading attachments (#224) 25 | - Add functions to retrieve avatars (#234) 26 | - Add support for stickers and sticker packs (#178) 27 | 28 | ### Fixed 29 | 30 | - Fix loading state store created with `0.5.1` (#169) 31 | - Stop incoming message loop from stopping in some cases (#172) 32 | - Fix a lot of issues related to (re)connecting to websockets (#177, #203, #222, #226) 33 | - Fix `presage-cli` argument parsing (#182) 34 | - Trust new identities by default (like the official clients do) (#206) 35 | - Send own profile key in protobuf messages (#208) 36 | - Fix outgoing messages stored in the wrong conversation thread (#216) 37 | - Fix linking as secondary device (#221) 38 | - Drop kyber keys on relinking and fix various related errors (#223, #225) 39 | - Fix expire timer being turned off (#226) 40 | - Avoid dropping database when it cannot be opend (#236) 41 | 42 | ### Changed 43 | 44 | - Store decrypted groups (avoiding wasting CPU resources) (#161) 45 | - Split code into multiple modules for eased maintenance (#205) 46 | - Upsert contact information when seeing a contact for the first time (#211) 47 | 48 | ## [0.5.2] 49 | 50 | ### Added 51 | 52 | - Set registration for PNI (phone-number identity) which will be fully implemented later. (#164) 53 | 54 | ### Fixed 55 | 56 | - Fix synchronization issue in the `sled` store implementation which could lead to corrupted sessions. (#162) 57 | - Don't reuse websocket when sending unidentified messages. (#165) 58 | - Fix fetching groups v2 metadata. (#164) 59 | 60 | ### Changed 61 | 62 | - `Manager::load_registered` is now an async method (small breaking change, sorry!). (#164) 63 | 64 | ## [0.5.1] 65 | 66 | Note: this release splits the project into multiple crates, to prepare for adding concurrent store implementations. 67 | While this might seem like a breaking change, the API has not been altered and your `Cargo.toml` should now look like: 68 | 69 | ```toml 70 | [dependencies] 71 | presage = { git = "https://github.com/whisperfish/presage" } 72 | presage-store-sled = { git = "https://github.com/whisperfish/presage" } 73 | ``` 74 | 75 | and then get the store implementation from the store crate instead when importing it like `use presage_store_sled::SledStore;`. 76 | 77 | ### Added 78 | 79 | - Add `Manager::submit_recaptcha_challenge`. (#143) 80 | - Cache profile API responses. (#134) 81 | - Add `is_registered` method to the store trait. (#156) 82 | 83 | ### Fixed 84 | 85 | - Fix sending with example CLI. (#140) 86 | - Fix sending with example CLI. (#140) 87 | - Fix sending duplicate messages to group for other clients (like Signal Desktop). (#142) 88 | - Fix storing of outgoing messages. (#144) 89 | 90 | ### Changed 91 | 92 | - Handle message deletion sent by contacts. (#147) 93 | - Split `presage` into multiple crates, before introducing additional store implementations. (#148) 94 | - Messages are now sent, whenever possible (which should be all the time), as [sealed sender](https://signal.org/blog/sealed-sender/). [#159] 95 | - Split project into multiple crates. (#148) 96 | 97 | ## [0.5.0] 98 | 99 | ### Added 100 | 101 | - Group storage: group metadata is now stored in a local cache, to avoid issuing an API call whenever 102 | group details need to be looked up. (#88) 103 | - Optional desktop notifications in CLI. (#85) 104 | - Add function to clear messages only. (#115) 105 | 106 | ### Fixed 107 | 108 | - Fix get contact by ID method. (#91) 109 | - Fix the key used when storing messages. (#111) 110 | - Fix unlink when clearing store. (#112) 111 | 112 | ### Changed 113 | 114 | - Improve sending messages speed by updating `libsignal-service-rs` and using its websocket in duplex mode (#92). Because of this change, polling on the stream returned by `Manager::receive_messages` is now required to send messages. 115 | - Only `DataMessage` that are sent, received, or sent from another device are saved in the local store (#137). 116 | - Changed (and fixed) the behaviour of the iterator returned by `SledStore::messages` (#119) 117 | * The iterator yields elements in chronological order (used to be reversed). 118 | * The iterator now implements `DoubleEndedIterator` which means you it can be reversed or consumed from the end. 119 | * The method now accepts the full range syntax, like `0..=1678295210` or `..` for all messages. 120 | - Wait for contacts sync to be received and processed when linking as secondary device. (#106) 121 | - Encrypt registration data (when store encryption is enabled). (#114) 122 | 123 | [0.5.0]: https://github.com/whisperfish/presage/compare/0.4.0...0.5.0 124 | [0.5.1]: https://github.com/whisperfish/presage/compare/0.5.0...0.5.1 125 | [0.5.2]: https://github.com/whisperfish/presage/compare/0.5.1...0.5.2 126 | [0.6.1]: https://github.com/whisperfish/presage/compare/0.5.2...0.6.1 127 | [Unreleased]: https://github.com/whisperfish/presage/compare/0.6.1...main 128 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["presage", "presage-cli", "presage-store-sled", "presage-store-cipher", "presage-store-sqlite"] 3 | resolver = "2" 4 | 5 | [patch.crates-io] 6 | curve25519-dalek = { git = 'https://github.com/signalapp/curve25519-dalek', tag = 'signal-curve25519-4.1.3' } 7 | 8 | # [patch."https://github.com/whisperfish/libsignal-service-rs.git"] 9 | # libsignal-service = { path = "../libsignal-service-rs" } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Presage 2 | 3 | ![CI Build](https://github.com/whisperfish/presage/workflows/Build/badge.svg) 4 | ![License](https://img.shields.io/github/license/whisperfish/presage) 5 | [![API Docs](https://img.shields.io/badge/docs-presage-blue)](https://whisperfish.github.io/presage/presage) 6 | 7 | A Rust library that helps building clients for the [Signal Messenger](https://signal.org/en/), using [libsignal-service-rs](https://github.com/whisperfish/libsignal-service-rs). It is designed to provide everything you need to get started. 8 | 9 | Features: 10 | 11 | - [x] Local storage with encryption: 12 | - [x] with [sled](https://github.com/spacejam/sled) 13 | - [ ] with [sqlx](https://crates.io/sqlx) and `sqlite` (see #287) 14 | - [x] Registration 15 | - [x] SMS 16 | - [x] Voice call 17 | - [x] Link as secondary device from Android / iOS app (like Signal Desktop) 18 | - [x] Contacts (synchronized from primary device) and profiles 19 | - [x] Groups 20 | - [x] Messages (incoming and outgoing) 21 | - [x] Fetch, decrypt and store attachments 22 | 23 | ## Instructions 24 | 25 | Add the following to your `Cargo.toml`: 26 | 27 | ```toml 28 | [dependencies] 29 | presage = { git = "https://github.com/whisperfish/presage" } 30 | presage-store-sled = { git = "https://github.com/whisperfish/presage" } 31 | 32 | # For a discussion as to why, see: 33 | # https://github.com/whisperfish/libsignal-service-rs/tree/93c23cf27d27a17a803e34ea3dd6a82d268fa79e#working-around-the-issue-with-curve25519-dalek 34 | [patch.crates-io] 35 | curve25519-dalek = { git = 'https://github.com/signalapp/curve25519-dalek', tag = 'signal-curve25519-4.1.3' } 36 | ``` 37 | 38 | and look at the generated Rust documentation of the `Manager` struct to get started. 39 | 40 | ## Demo CLI 41 | 42 | Included in this repository is a nearly fully functional CLI that can serve as an example to build your client (you can also use it to query your `presage` database): 43 | 44 | ``` 45 | # print help section 46 | cargo run -- --help 47 | 48 | # link as secondary device, a PNG with a QR code to scan should open 49 | cargo run -- link-device --device-name presage 50 | 51 | # start receiving messages 52 | cargo run -- receive 53 | ``` 54 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | database_url := ("sqlite://" + justfile_directory() + "/test.db") 2 | 3 | prepare-sqlx: setup-sqlx-db 4 | cargo sqlx prepare --workspace --database-url "{{database_url}}" 5 | 6 | [working-directory: "presage-store-sqlite"] 7 | setup-sqlx-db: 8 | cargo sqlx database setup --database-url "{{database_url}}" 9 | 10 | install-sqlx: 11 | cargo binstall sqlx-cli@v0.8.3 12 | -------------------------------------------------------------------------------- /presage-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "presage-cli" 3 | version = "0.6.0-dev" 4 | edition = "2021" 5 | authors = ["Gabriel Féron "] 6 | license = "AGPL-3.0-only" 7 | 8 | [dependencies] 9 | presage = { path = "../presage" } 10 | presage-store-sled = { path = "../presage-store-sled" } 11 | presage-store-sqlite = { path = "../presage-store-sqlite" } 12 | 13 | anyhow = { version = "1.0", features = ["backtrace"] } 14 | base64 = "0.22" 15 | chrono = { version = "0.4", default-features = false, features = ["serde", "clock"] } 16 | clap = { version = ">=4.2.4", features = ["derive"] } 17 | directories = "5.0" 18 | env_logger = "0.11" 19 | futures = "0.3" 20 | hex = "0.4" 21 | mime_guess = "2.0" 22 | notify-rust = "4.10.0" 23 | qr2term = { version = "0.3.1" } 24 | tempfile = "3.9" 25 | tokio = { version = "1.43", features = ["macros", "rt-multi-thread", "io-std", "io-util"] } 26 | tracing = "0.1" 27 | tracing-subscriber = { version = "0.3", default-features = false } 28 | url = "2.5" 29 | -------------------------------------------------------------------------------- /presage-store-cipher/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "presage-store-cipher" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "AGPL-3.0-only" 6 | 7 | [dependencies] 8 | blake3 = "1.5.0" 9 | chacha20poly1305 = { version = "0.10.1", features = ["std"] } 10 | hmac = "0.12.1" 11 | pbkdf2 = "0.12.2" 12 | rand = "0.8" 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_json = "1.0" 15 | sha2 = "0.10" 16 | thiserror = "1.0" 17 | zeroize = { version = "1.7.0", features = ["derive"] } 18 | -------------------------------------------------------------------------------- /presage-store-cipher/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Based on `matrix-sdk-store-encryption` (License Apache-2.0) 2 | 3 | use blake3::{derive_key, Hash}; 4 | use chacha20poly1305::aead::Aead; 5 | use chacha20poly1305::{AeadCore, KeyInit, XChaCha20Poly1305, XNonce}; 6 | use hmac::Hmac; 7 | use pbkdf2::pbkdf2; 8 | use rand::{thread_rng, RngCore}; 9 | use serde::de::DeserializeOwned; 10 | use serde::{Deserialize, Serialize}; 11 | use sha2::Sha256; 12 | 13 | use zeroize::{Zeroize, Zeroizing}; 14 | 15 | const VERSION: u8 = 1; 16 | const KDF_SALT_SIZE: usize = 32; 17 | const XNONCE_SIZE: usize = 24; 18 | const KDF_ROUNDS: u32 = 200_000; 19 | 20 | /// Hashes keys and encrypts/decrypts values 21 | /// 22 | /// Allows to encrypt/decrypt data in a key/value store. Can be exported as bytes encrypted by a 23 | /// passphrase, and imported back from bytes. 24 | #[derive(Zeroize)] 25 | #[zeroize(drop)] 26 | pub struct StoreCipher { 27 | encryption_key: Box<[u8; 32]>, 28 | mac_key_seed: Box<[u8; 32]>, 29 | } 30 | 31 | impl StoreCipher { 32 | pub fn new() -> Self { 33 | let mut rng = thread_rng(); 34 | let mut store_cipher = Self::zero(); 35 | rng.fill_bytes(store_cipher.encryption_key.as_mut_slice()); 36 | rng.fill_bytes(store_cipher.mac_key_seed.as_mut_slice()); 37 | store_cipher 38 | } 39 | 40 | pub fn export(&self, passphrase: &str) -> Result, StoreCipherError> { 41 | self.export_inner(passphrase, KDF_ROUNDS) 42 | } 43 | 44 | pub fn insecure_export_fast_for_testing( 45 | &self, 46 | passphrase: &str, 47 | ) -> Result, StoreCipherError> { 48 | self.export_inner(passphrase, 1000) 49 | } 50 | 51 | pub(crate) fn export_inner( 52 | &self, 53 | passphrase: &str, 54 | rounds: u32, 55 | ) -> Result, StoreCipherError> { 56 | let mut rng = thread_rng(); 57 | let mut salt = [0u8; KDF_SALT_SIZE]; 58 | rng.fill_bytes(&mut salt); 59 | 60 | let key = StoreCipher::expand_key(passphrase, &salt, rounds); 61 | let key = chacha20poly1305::Key::from(key); 62 | let cipher = XChaCha20Poly1305::new(&key); 63 | 64 | let nonce = XChaCha20Poly1305::generate_nonce(rng); 65 | 66 | let mut keys = Zeroizing::new([0u8; 64]); 67 | keys[0..32].copy_from_slice(&*self.encryption_key); 68 | keys[32..64].copy_from_slice(&*self.mac_key_seed); 69 | 70 | let ciphertext = cipher.encrypt(&nonce, keys.as_slice())?; 71 | 72 | let store_cipher = EncryptedStoreCipher { 73 | kdf_info: KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds, salt }, 74 | ciphertext_info: CipherTextInfo::ChaCha20Poly1305 { 75 | nonce: nonce.as_slice().try_into().expect("invalid array len"), 76 | ciphertext, 77 | }, 78 | }; 79 | Ok(serde_json::to_vec(&store_cipher)?) 80 | } 81 | 82 | pub fn import(passphrase: &str, encrypted: &[u8]) -> Result { 83 | let encrypted: EncryptedStoreCipher = serde_json::from_slice(encrypted)?; 84 | let key = match encrypted.kdf_info { 85 | KdfInfo::Pbkdf2ToChaCha20Poly1305 { 86 | rounds, 87 | salt: kdf_salt, 88 | } => Self::expand_key(passphrase, &kdf_salt, rounds), 89 | }; 90 | 91 | let key = chacha20poly1305::Key::from(key); 92 | 93 | let decrypted = match encrypted.ciphertext_info { 94 | CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext } => { 95 | let cipher = XChaCha20Poly1305::new(&key); 96 | let nonce = XNonce::from_slice(&nonce); 97 | Zeroizing::new(cipher.decrypt(nonce, &*ciphertext)?) 98 | } 99 | }; 100 | 101 | if decrypted.len() != 64 { 102 | return Err(StoreCipherError::Length(64, decrypted.len())); 103 | } 104 | 105 | let mut store_cipher = Self::zero(); 106 | store_cipher 107 | .encryption_key 108 | .copy_from_slice(&decrypted[0..32]); 109 | store_cipher 110 | .mac_key_seed 111 | .copy_from_slice(&decrypted[32..64]); 112 | Ok(store_cipher) 113 | } 114 | 115 | fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> [u8; 32] { 116 | let mut key = [0u8; 32]; 117 | pbkdf2::>(passphrase.as_bytes(), salt, rounds, &mut key) 118 | .expect("invalid length"); 119 | key 120 | } 121 | 122 | pub fn encrypt_value(&self, value: &impl Serialize) -> Result, StoreCipherError> { 123 | Ok(serde_json::to_vec(&self.encrypt_value_typed(value)?)?) 124 | } 125 | 126 | fn encrypt_value_typed( 127 | &self, 128 | value: &impl Serialize, 129 | ) -> Result { 130 | let data = serde_json::to_vec(value)?; 131 | self.encrypt_value_data(data) 132 | } 133 | 134 | fn encrypt_value_data(&self, mut data: Vec) -> Result { 135 | let nonce = XChaCha20Poly1305::generate_nonce(thread_rng()); 136 | let cipher = XChaCha20Poly1305::new(self.encryption_key()); 137 | 138 | let ciphertext = cipher.encrypt(&nonce, &*data)?; 139 | 140 | data.zeroize(); 141 | Ok(EncryptedValue { 142 | version: VERSION, 143 | ciphertext, 144 | nonce: nonce.as_slice().try_into().expect("invalid array len"), 145 | }) 146 | } 147 | 148 | pub fn decrypt_value(&self, value: &[u8]) -> Result { 149 | let value: EncryptedValue = serde_json::from_slice(value)?; 150 | self.decrypt_value_typed(value) 151 | } 152 | 153 | fn decrypt_value_typed( 154 | &self, 155 | value: EncryptedValue, 156 | ) -> Result { 157 | let mut plaintext = self.decrypt_value_data(value)?; 158 | let ret = serde_json::from_slice(&plaintext); 159 | plaintext.zeroize(); 160 | Ok(ret?) 161 | } 162 | 163 | fn decrypt_value_data(&self, value: EncryptedValue) -> Result, StoreCipherError> { 164 | if value.version != VERSION { 165 | return Err(StoreCipherError::Version(VERSION, value.version)); 166 | } 167 | 168 | let cipher = XChaCha20Poly1305::new(self.encryption_key()); 169 | let nonce = XNonce::from_slice(&value.nonce); 170 | Ok(cipher.decrypt(nonce, &*value.ciphertext)?) 171 | } 172 | 173 | pub fn hash_key(&self, table_name: &str, key: &[u8]) -> [u8; 32] { 174 | let mac_key = self.get_mac_key_for_table(table_name); 175 | mac_key.mac(key).into() 176 | } 177 | 178 | fn get_mac_key_for_table(&self, table_name: &str) -> MacKey { 179 | let mut key = MacKey(Box::new([0u8; 32])); 180 | let output = Zeroizing::new(derive_key(table_name, &*self.mac_key_seed)); 181 | key.0.copy_from_slice(&*output); 182 | key 183 | } 184 | 185 | fn encryption_key(&self) -> &chacha20poly1305::Key { 186 | chacha20poly1305::Key::from_slice(&*self.encryption_key) 187 | } 188 | 189 | fn zero() -> StoreCipher { 190 | Self { 191 | encryption_key: Box::new([0; 32]), 192 | mac_key_seed: Box::new([0; 32]), 193 | } 194 | } 195 | } 196 | 197 | #[derive(Zeroize)] 198 | #[zeroize(drop)] 199 | struct MacKey(Box<[u8; 32]>); 200 | 201 | impl MacKey { 202 | fn mac(&self, input: &[u8]) -> Hash { 203 | blake3::keyed_hash(&self.0, input) 204 | } 205 | } 206 | 207 | impl Default for StoreCipher { 208 | fn default() -> Self { 209 | Self::new() 210 | } 211 | } 212 | 213 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 214 | struct EncryptedValue { 215 | version: u8, 216 | ciphertext: Vec, 217 | nonce: [u8; XNONCE_SIZE], 218 | } 219 | 220 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 221 | enum KdfInfo { 222 | Pbkdf2ToChaCha20Poly1305 { 223 | rounds: u32, 224 | salt: [u8; KDF_SALT_SIZE], 225 | }, 226 | } 227 | 228 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 229 | enum CipherTextInfo { 230 | ChaCha20Poly1305 { 231 | nonce: [u8; XNONCE_SIZE], 232 | ciphertext: Vec, 233 | }, 234 | } 235 | 236 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 237 | struct EncryptedStoreCipher { 238 | pub kdf_info: KdfInfo, 239 | pub ciphertext_info: CipherTextInfo, 240 | } 241 | 242 | #[derive(Debug, thiserror::Error)] 243 | pub enum StoreCipherError { 244 | #[error(transparent)] 245 | Serde(#[from] serde_json::Error), 246 | #[error("unsupported data version, expected {0}, got {1}")] 247 | Version(u8, u8), 248 | #[error(transparent)] 249 | Encryption(#[from] chacha20poly1305::aead::Error), 250 | #[error("invalid ciphertext length, expected {0}, got {1}")] 251 | Length(usize, usize), 252 | } 253 | 254 | #[cfg(test)] 255 | mod tests { 256 | use serde_json::{json, Value}; 257 | 258 | use super::*; 259 | 260 | #[test] 261 | fn test_export_import() -> Result<(), StoreCipherError> { 262 | let passphrase = "The first rule of Fight Club is: you do not talk about Fight Club."; 263 | let store_cipher = StoreCipher::new(); 264 | 265 | let value = json!({"name": "Tyler Durden"}); 266 | let encrypted_value = store_cipher.encrypt_value(&value)?; 267 | 268 | let encrypted = store_cipher.insecure_export_fast_for_testing(passphrase)?; 269 | let decrypted = StoreCipher::import(passphrase, &encrypted)?; 270 | 271 | assert_eq!(store_cipher.encryption_key, decrypted.encryption_key); 272 | 273 | let decrypted_value: Value = decrypted.decrypt_value(&encrypted_value)?; 274 | assert_eq!(value, decrypted_value); 275 | 276 | Ok(()) 277 | } 278 | 279 | #[test] 280 | fn test_encrypt_decrypt() -> Result<(), StoreCipherError> { 281 | let store_cipher = StoreCipher::new(); 282 | 283 | let value = json!({"name": "Tyler Durden"}); 284 | let encrypted_value = store_cipher.encrypt_value(&value)?; 285 | let decrypted_value: Value = store_cipher.decrypt_value(&encrypted_value)?; 286 | assert_eq!(value, decrypted_value); 287 | 288 | Ok(()) 289 | } 290 | 291 | #[test] 292 | fn test_hash_key() { 293 | let store_cipher = StoreCipher::new(); 294 | let k1 = store_cipher.hash_key("movie", b"Fight Club"); 295 | let k2 = store_cipher.hash_key("movie", b"Fight Club"); 296 | assert_eq!(k1, k2); 297 | let k3 = store_cipher.hash_key("movie", b"Fifth Element"); 298 | assert_ne!(k1, k3); 299 | let k4 = store_cipher.hash_key("film", b"Fight Club"); 300 | assert_ne!(k1, k4); 301 | assert_ne!(k3, k4); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /presage-store-sled/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "presage-store-sled" 3 | version = "0.6.0-dev" 4 | edition = "2021" 5 | authors = ["Gabriel Féron "] 6 | license = "AGPL-3.0-only" 7 | 8 | [dependencies] 9 | presage = { path = "../presage" } 10 | presage-store-cipher = { path = "../presage-store-cipher", optional = true } 11 | 12 | async-trait = "0.1" 13 | base64 = "0.22" 14 | chrono = "0.4.35" 15 | fs_extra = "1.3" 16 | prost = "0.13" 17 | quickcheck_macros = "1.0.0" 18 | serde = { version = "1.0", features = ["derive"] } 19 | serde_json = "1.0" 20 | sha2 = "0.10" 21 | sled = { version = "0.34" } 22 | thiserror = "1.0" 23 | tracing = "0.1" 24 | 25 | [build-dependencies] 26 | prost-build = "0.13" 27 | 28 | [dev-dependencies] 29 | anyhow = "1.0" 30 | futures = "0.3" 31 | quickcheck = "1.0.3" 32 | quickcheck_async = "0.1" 33 | rand = "0.8" 34 | tokio = { version = "1.43", default-features = false, features = ["time"] } 35 | 36 | [features] 37 | default = ["encryption"] 38 | encryption = ["dep:presage-store-cipher"] 39 | -------------------------------------------------------------------------------- /presage-store-sled/build.rs: -------------------------------------------------------------------------------- 1 | use std::io::Result; 2 | use std::path::Path; 3 | 4 | fn main() -> Result<()> { 5 | let protobuf = Path::new("src/protobuf").to_owned(); 6 | 7 | // Build script does not automagically rerun when a new protobuf file is added. 8 | // Directories are checked against mtime, which is platform specific 9 | println!("cargo:rerun-if-changed=src/protobuf"); 10 | 11 | let input: Vec<_> = protobuf 12 | .read_dir() 13 | .expect("protobuf directory") 14 | .filter_map(|entry| { 15 | let entry = entry.expect("readable protobuf directory"); 16 | let path = entry.path(); 17 | if Some("proto") == path.extension().and_then(std::ffi::OsStr::to_str) { 18 | assert!(path.is_file()); 19 | println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); 20 | Some(path) 21 | } else { 22 | None 23 | } 24 | }) 25 | .collect(); 26 | 27 | prost_build::compile_protos(&input, &[protobuf])?; 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /presage-store-sled/src/content.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ops::{Bound, RangeBounds, RangeFull}, 3 | sync::Arc, 4 | }; 5 | 6 | use presage::{ 7 | libsignal_service::{ 8 | content::Content, 9 | prelude::Uuid, 10 | zkgroup::{profiles::ProfileKey, GroupMasterKeyBytes}, 11 | Profile, 12 | }, 13 | model::{contacts::Contact, groups::Group}, 14 | store::{ContentExt, ContentsStore, StickerPack, Thread}, 15 | AvatarBytes, 16 | }; 17 | use prost::Message; 18 | use serde::de::DeserializeOwned; 19 | use sha2::{Digest, Sha256}; 20 | use sled::IVec; 21 | use tracing::{debug, trace}; 22 | 23 | use crate::{protobuf::ContentProto, SledStore, SledStoreError}; 24 | 25 | const SLED_TREE_PROFILE_AVATARS: &str = "profile_avatars"; 26 | const SLED_TREE_PROFILE_KEYS: &str = "profile_keys"; 27 | const SLED_TREE_STICKER_PACKS: &str = "sticker_packs"; 28 | const SLED_TREE_CONTACTS: &str = "contacts"; 29 | const SLED_TREE_GROUP_AVATARS: &str = "group_avatars"; 30 | const SLED_TREE_GROUPS: &str = "groups"; 31 | const SLED_TREE_PROFILES: &str = "profiles"; 32 | const SLED_TREE_THREADS_PREFIX: &str = "threads"; 33 | 34 | impl ContentsStore for SledStore { 35 | type ContentsStoreError = SledStoreError; 36 | 37 | type ContactsIter = SledContactsIter; 38 | type GroupsIter = SledGroupsIter; 39 | type MessagesIter = SledMessagesIter; 40 | type StickerPacksIter = SledStickerPacksIter; 41 | 42 | async fn clear_profiles(&mut self) -> Result<(), Self::ContentsStoreError> { 43 | let db = self.write(); 44 | db.drop_tree(SLED_TREE_PROFILES)?; 45 | db.drop_tree(SLED_TREE_PROFILE_KEYS)?; 46 | db.drop_tree(SLED_TREE_PROFILE_AVATARS)?; 47 | db.flush()?; 48 | Ok(()) 49 | } 50 | 51 | async fn clear_contents(&mut self) -> Result<(), Self::ContentsStoreError> { 52 | let db = self.write(); 53 | db.drop_tree(SLED_TREE_CONTACTS)?; 54 | db.drop_tree(SLED_TREE_GROUPS)?; 55 | 56 | for tree in db 57 | .tree_names() 58 | .into_iter() 59 | .filter(|n| n.starts_with(SLED_TREE_THREADS_PREFIX.as_bytes())) 60 | { 61 | db.drop_tree(tree)?; 62 | } 63 | 64 | db.flush()?; 65 | Ok(()) 66 | } 67 | 68 | async fn clear_contacts(&mut self) -> Result<(), SledStoreError> { 69 | self.write().drop_tree(SLED_TREE_CONTACTS)?; 70 | Ok(()) 71 | } 72 | 73 | async fn save_contact(&mut self, contact: &Contact) -> Result<(), SledStoreError> { 74 | self.insert(SLED_TREE_CONTACTS, contact.uuid, contact)?; 75 | debug!("saved contact"); 76 | Ok(()) 77 | } 78 | 79 | async fn contacts(&self) -> Result { 80 | Ok(SledContactsIter { 81 | iter: self.read().open_tree(SLED_TREE_CONTACTS)?.iter(), 82 | #[cfg(feature = "encryption")] 83 | cipher: self.cipher.clone(), 84 | }) 85 | } 86 | 87 | async fn contact_by_id(&self, id: &Uuid) -> Result, SledStoreError> { 88 | self.get(SLED_TREE_CONTACTS, id) 89 | } 90 | 91 | // Groups 92 | 93 | async fn clear_groups(&mut self) -> Result<(), SledStoreError> { 94 | let db = self.write(); 95 | db.drop_tree(SLED_TREE_GROUPS)?; 96 | db.flush()?; 97 | Ok(()) 98 | } 99 | 100 | async fn groups(&self) -> Result { 101 | Ok(SledGroupsIter { 102 | iter: self.read().open_tree(SLED_TREE_GROUPS)?.iter(), 103 | #[cfg(feature = "encryption")] 104 | cipher: self.cipher.clone(), 105 | }) 106 | } 107 | 108 | async fn group( 109 | &self, 110 | master_key_bytes: GroupMasterKeyBytes, 111 | ) -> Result, SledStoreError> { 112 | self.get(SLED_TREE_GROUPS, master_key_bytes) 113 | } 114 | 115 | async fn save_group( 116 | &self, 117 | master_key: GroupMasterKeyBytes, 118 | group: impl Into, 119 | ) -> Result<(), SledStoreError> { 120 | self.insert(SLED_TREE_GROUPS, master_key, group.into())?; 121 | Ok(()) 122 | } 123 | 124 | async fn group_avatar( 125 | &self, 126 | master_key_bytes: GroupMasterKeyBytes, 127 | ) -> Result, SledStoreError> { 128 | self.get(SLED_TREE_GROUP_AVATARS, master_key_bytes) 129 | } 130 | 131 | async fn save_group_avatar( 132 | &self, 133 | master_key: GroupMasterKeyBytes, 134 | avatar: &AvatarBytes, 135 | ) -> Result<(), SledStoreError> { 136 | self.insert(SLED_TREE_GROUP_AVATARS, master_key, avatar)?; 137 | Ok(()) 138 | } 139 | 140 | // Messages 141 | 142 | async fn clear_messages(&mut self) -> Result<(), SledStoreError> { 143 | let db = self.write(); 144 | for name in db.tree_names() { 145 | if name 146 | .as_ref() 147 | .starts_with(SLED_TREE_THREADS_PREFIX.as_bytes()) 148 | { 149 | db.drop_tree(&name)?; 150 | } 151 | } 152 | db.flush()?; 153 | Ok(()) 154 | } 155 | 156 | async fn clear_thread(&mut self, thread: &Thread) -> Result<(), SledStoreError> { 157 | trace!(%thread, "clearing thread"); 158 | 159 | let db = self.write(); 160 | db.drop_tree(messages_thread_tree_name(thread))?; 161 | db.flush()?; 162 | 163 | Ok(()) 164 | } 165 | 166 | async fn save_message(&self, thread: &Thread, message: Content) -> Result<(), SledStoreError> { 167 | let ts = message.timestamp(); 168 | trace!(%thread, ts, "storing a message with thread"); 169 | 170 | let tree = messages_thread_tree_name(thread); 171 | let key = ts.to_be_bytes(); 172 | 173 | let proto: ContentProto = message.into(); 174 | let value = proto.encode_to_vec(); 175 | 176 | self.insert(&tree, key, value)?; 177 | 178 | Ok(()) 179 | } 180 | 181 | async fn delete_message( 182 | &mut self, 183 | thread: &Thread, 184 | timestamp: u64, 185 | ) -> Result { 186 | let tree = messages_thread_tree_name(thread); 187 | self.remove(&tree, timestamp.to_be_bytes()) 188 | } 189 | 190 | async fn message( 191 | &self, 192 | thread: &Thread, 193 | timestamp: u64, 194 | ) -> Result, SledStoreError> { 195 | // Big-Endian needed, otherwise wrong ordering in sled. 196 | let val: Option> = 197 | self.get(&messages_thread_tree_name(thread), timestamp.to_be_bytes())?; 198 | match val { 199 | Some(ref v) => { 200 | let proto = ContentProto::decode(v.as_slice())?; 201 | let content = proto.try_into()?; 202 | Ok(Some(content)) 203 | } 204 | None => Ok(None), 205 | } 206 | } 207 | 208 | async fn messages( 209 | &self, 210 | thread: &Thread, 211 | range: impl RangeBounds, 212 | ) -> Result { 213 | let tree_thread = self.read().open_tree(messages_thread_tree_name(thread))?; 214 | debug!(%thread, count = tree_thread.len(), "loading message tree"); 215 | 216 | let iter = match (range.start_bound(), range.end_bound()) { 217 | (Bound::Included(start), Bound::Unbounded) => tree_thread.range(start.to_be_bytes()..), 218 | (Bound::Included(start), Bound::Excluded(end)) => { 219 | tree_thread.range(start.to_be_bytes()..end.to_be_bytes()) 220 | } 221 | (Bound::Included(start), Bound::Included(end)) => { 222 | tree_thread.range(start.to_be_bytes()..=end.to_be_bytes()) 223 | } 224 | (Bound::Unbounded, Bound::Included(end)) => tree_thread.range(..=end.to_be_bytes()), 225 | (Bound::Unbounded, Bound::Excluded(end)) => tree_thread.range(..end.to_be_bytes()), 226 | (Bound::Unbounded, Bound::Unbounded) => tree_thread.range::<[u8; 8], RangeFull>(..), 227 | (Bound::Excluded(_), _) => { 228 | unreachable!("range that excludes the initial value") 229 | } 230 | }; 231 | 232 | Ok(SledMessagesIter { 233 | #[cfg(feature = "encryption")] 234 | cipher: self.cipher.clone(), 235 | iter, 236 | }) 237 | } 238 | 239 | async fn upsert_profile_key( 240 | &mut self, 241 | uuid: &Uuid, 242 | key: ProfileKey, 243 | ) -> Result { 244 | self.insert(SLED_TREE_PROFILE_KEYS, uuid.as_bytes(), key) 245 | } 246 | 247 | async fn profile_key(&self, uuid: &Uuid) -> Result, SledStoreError> { 248 | self.get(SLED_TREE_PROFILE_KEYS, uuid.as_bytes()) 249 | } 250 | 251 | async fn save_profile( 252 | &mut self, 253 | uuid: Uuid, 254 | key: ProfileKey, 255 | profile: Profile, 256 | ) -> Result<(), SledStoreError> { 257 | let key = self.profile_key_for_uuid(uuid, key); 258 | self.insert(SLED_TREE_PROFILES, key, profile)?; 259 | Ok(()) 260 | } 261 | 262 | async fn profile( 263 | &self, 264 | uuid: Uuid, 265 | key: ProfileKey, 266 | ) -> Result, SledStoreError> { 267 | let key = self.profile_key_for_uuid(uuid, key); 268 | self.get(SLED_TREE_PROFILES, key) 269 | } 270 | 271 | async fn save_profile_avatar( 272 | &mut self, 273 | uuid: Uuid, 274 | key: ProfileKey, 275 | avatar: &AvatarBytes, 276 | ) -> Result<(), SledStoreError> { 277 | let key = self.profile_key_for_uuid(uuid, key); 278 | self.insert(SLED_TREE_PROFILE_AVATARS, key, avatar)?; 279 | Ok(()) 280 | } 281 | 282 | async fn profile_avatar( 283 | &self, 284 | uuid: Uuid, 285 | key: ProfileKey, 286 | ) -> Result, SledStoreError> { 287 | let key = self.profile_key_for_uuid(uuid, key); 288 | self.get(SLED_TREE_PROFILE_AVATARS, key) 289 | } 290 | 291 | async fn add_sticker_pack(&mut self, pack: &StickerPack) -> Result<(), SledStoreError> { 292 | self.insert(SLED_TREE_STICKER_PACKS, pack.id.clone(), pack)?; 293 | Ok(()) 294 | } 295 | 296 | async fn remove_sticker_pack(&mut self, id: &[u8]) -> Result { 297 | self.remove(SLED_TREE_STICKER_PACKS, id) 298 | } 299 | 300 | async fn sticker_pack(&self, id: &[u8]) -> Result, SledStoreError> { 301 | self.get(SLED_TREE_STICKER_PACKS, id) 302 | } 303 | 304 | async fn sticker_packs(&self) -> Result { 305 | Ok(SledStickerPacksIter { 306 | cipher: self.cipher.clone(), 307 | iter: self.read().open_tree(SLED_TREE_STICKER_PACKS)?.iter(), 308 | }) 309 | } 310 | } 311 | 312 | pub struct SledContactsIter { 313 | #[cfg(feature = "encryption")] 314 | cipher: Option>, 315 | iter: sled::Iter, 316 | } 317 | 318 | impl SledContactsIter { 319 | #[cfg(feature = "encryption")] 320 | fn decrypt_value(&self, value: &[u8]) -> Result { 321 | if let Some(cipher) = self.cipher.as_ref() { 322 | Ok(cipher.decrypt_value(value)?) 323 | } else { 324 | Ok(serde_json::from_slice(value)?) 325 | } 326 | } 327 | 328 | #[cfg(not(feature = "encryption"))] 329 | fn decrypt_value(&self, value: &[u8]) -> Result { 330 | Ok(serde_json::from_slice(value)?) 331 | } 332 | } 333 | 334 | impl Iterator for SledContactsIter { 335 | type Item = Result; 336 | 337 | fn next(&mut self) -> Option { 338 | self.iter 339 | .next()? 340 | .map_err(SledStoreError::from) 341 | .and_then(|(_key, value)| self.decrypt_value(&value)) 342 | .into() 343 | } 344 | } 345 | 346 | pub struct SledGroupsIter { 347 | #[cfg(feature = "encryption")] 348 | cipher: Option>, 349 | iter: sled::Iter, 350 | } 351 | 352 | impl SledGroupsIter { 353 | #[cfg(feature = "encryption")] 354 | fn decrypt_value(&self, value: &[u8]) -> Result { 355 | if let Some(cipher) = self.cipher.as_ref() { 356 | Ok(cipher.decrypt_value(value)?) 357 | } else { 358 | Ok(serde_json::from_slice(value)?) 359 | } 360 | } 361 | 362 | #[cfg(not(feature = "encryption"))] 363 | fn decrypt_value(&self, value: &[u8]) -> Result { 364 | Ok(serde_json::from_slice(value)?) 365 | } 366 | } 367 | 368 | impl Iterator for SledGroupsIter { 369 | type Item = Result<(GroupMasterKeyBytes, Group), SledStoreError>; 370 | 371 | fn next(&mut self) -> Option { 372 | Some(self.iter.next()?.map_err(SledStoreError::from).and_then( 373 | |(group_master_key_bytes, value)| { 374 | let group = self.decrypt_value(&value)?; 375 | Ok(( 376 | group_master_key_bytes 377 | .as_ref() 378 | .try_into() 379 | .map_err(|_| SledStoreError::GroupDecryption)?, 380 | group, 381 | )) 382 | }, 383 | )) 384 | } 385 | } 386 | 387 | pub struct SledStickerPacksIter { 388 | #[cfg(feature = "encryption")] 389 | cipher: Option>, 390 | iter: sled::Iter, 391 | } 392 | 393 | impl Iterator for SledStickerPacksIter { 394 | type Item = Result; 395 | 396 | #[cfg(feature = "encryption")] 397 | fn next(&mut self) -> Option { 398 | self.iter 399 | .next()? 400 | .map_err(SledStoreError::from) 401 | .and_then(|(_key, value)| { 402 | if let Some(cipher) = self.cipher.as_ref() { 403 | cipher.decrypt_value(&value).map_err(SledStoreError::from) 404 | } else { 405 | serde_json::from_slice(&value).map_err(SledStoreError::from) 406 | } 407 | }) 408 | .into() 409 | } 410 | 411 | #[cfg(not(feature = "encryption"))] 412 | fn next(&mut self) -> Option { 413 | self.iter 414 | .next()? 415 | .map_err(SledStoreError::from) 416 | .and_then(|(_key, value)| serde_json::from_slice(&value).map_err(SledStoreError::from)) 417 | .into() 418 | } 419 | } 420 | 421 | pub struct SledMessagesIter { 422 | #[cfg(feature = "encryption")] 423 | cipher: Option>, 424 | iter: sled::Iter, 425 | } 426 | 427 | impl SledMessagesIter { 428 | #[cfg(feature = "encryption")] 429 | fn decrypt_value(&self, value: &[u8]) -> Result { 430 | if let Some(cipher) = self.cipher.as_ref() { 431 | Ok(cipher.decrypt_value(value)?) 432 | } else { 433 | Ok(serde_json::from_slice(value)?) 434 | } 435 | } 436 | 437 | #[cfg(not(feature = "encryption"))] 438 | fn decrypt_value(&self, value: &[u8]) -> Result { 439 | Ok(serde_json::from_slice(value)?) 440 | } 441 | } 442 | 443 | impl SledMessagesIter { 444 | fn decode( 445 | &self, 446 | elem: Result<(IVec, IVec), sled::Error>, 447 | ) -> Option> { 448 | elem.map_err(SledStoreError::from) 449 | .and_then(|(_, value)| self.decrypt_value(&value)) 450 | .and_then(|data: Vec| ContentProto::decode(&data[..]).map_err(SledStoreError::from)) 451 | .map_or_else(|e| Some(Err(e)), |p| Some(p.try_into())) 452 | } 453 | } 454 | 455 | impl Iterator for SledMessagesIter { 456 | type Item = Result; 457 | 458 | fn next(&mut self) -> Option { 459 | let elem = self.iter.next()?; 460 | self.decode(elem) 461 | } 462 | } 463 | 464 | impl DoubleEndedIterator for SledMessagesIter { 465 | fn next_back(&mut self) -> Option { 466 | let elem = self.iter.next_back()?; 467 | self.decode(elem) 468 | } 469 | } 470 | 471 | fn messages_thread_tree_name(t: &Thread) -> String { 472 | use base64::prelude::*; 473 | let key = match t { 474 | Thread::Contact(uuid) => { 475 | format!("{SLED_TREE_THREADS_PREFIX}:contact:{uuid}") 476 | } 477 | Thread::Group(group_id) => format!( 478 | "{SLED_TREE_THREADS_PREFIX}:group:{}", 479 | BASE64_STANDARD.encode(group_id) 480 | ), 481 | }; 482 | let mut hasher = Sha256::new(); 483 | hasher.update(key.as_bytes()); 484 | format!("{SLED_TREE_THREADS_PREFIX}:{:x}", hasher.finalize()) 485 | } 486 | -------------------------------------------------------------------------------- /presage-store-sled/src/error.rs: -------------------------------------------------------------------------------- 1 | use presage::{libsignal_service::protocol::SignalProtocolError, store::StoreError}; 2 | use tracing::error; 3 | 4 | #[derive(Debug, thiserror::Error)] 5 | #[non_exhaustive] 6 | pub enum SledStoreError { 7 | #[error("database migration is not supported")] 8 | MigrationConflict, 9 | #[error("data store error: {0}")] 10 | Db(#[from] sled::Error), 11 | #[error("data store error: {0}")] 12 | DbTransaction(#[from] sled::transaction::TransactionError), 13 | #[cfg(feature = "encryption")] 14 | #[error("store cipher error: {0}")] 15 | StoreCipher(#[from] presage_store_cipher::StoreCipherError), 16 | #[error("JSON error: {0}")] 17 | Json(#[from] serde_json::Error), 18 | #[error("base64 decode error: {0}")] 19 | Base64Decode(#[from] base64::DecodeError), 20 | #[error("Prost error: {0}")] 21 | ProtobufDecode(#[from] prost::DecodeError), 22 | #[error("I/O error: {0}")] 23 | FsExtra(#[from] fs_extra::error::Error), 24 | #[error("group decryption error")] 25 | GroupDecryption, 26 | #[error("No UUID")] 27 | NoUuid, 28 | #[error("Unsupported message content")] 29 | UnsupportedContent, 30 | #[error(transparent)] 31 | Protocol(#[from] SignalProtocolError), 32 | } 33 | 34 | impl StoreError for SledStoreError {} 35 | 36 | impl From for SignalProtocolError { 37 | fn from(error: SledStoreError) -> Self { 38 | error!(%error, "presage store error"); 39 | Self::InvalidState("presage store error", error.to_string()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /presage-store-sled/src/protobuf.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::derive_partial_eq_without_eq)] 2 | mod textsecure { 3 | include!(concat!(env!("OUT_DIR"), "/textsecure.rs")); 4 | } 5 | 6 | use std::str::FromStr; 7 | 8 | use presage::libsignal_service::content::Content; 9 | use presage::libsignal_service::content::ContentBody; 10 | use presage::libsignal_service::content::Metadata; 11 | use presage::libsignal_service::prelude::Uuid; 12 | use presage::libsignal_service::proto; 13 | use presage::libsignal_service::protocol::ServiceId; 14 | 15 | use crate::SledStoreError; 16 | 17 | use self::textsecure::AddressProto; 18 | use self::textsecure::MetadataProto; 19 | 20 | impl From for AddressProto { 21 | fn from(s: ServiceId) -> Self { 22 | AddressProto { 23 | uuid: Some(s.raw_uuid().as_bytes().to_vec()), 24 | } 25 | } 26 | } 27 | 28 | impl TryFrom for ServiceId { 29 | type Error = SledStoreError; 30 | 31 | fn try_from(address: AddressProto) -> Result { 32 | address 33 | .uuid 34 | .and_then(|bytes| Some(Uuid::from_bytes(bytes.try_into().ok()?))) 35 | .ok_or_else(|| SledStoreError::NoUuid) 36 | .map(|u| ServiceId::Aci(u.into())) 37 | } 38 | } 39 | 40 | impl From for MetadataProto { 41 | fn from(m: Metadata) -> Self { 42 | MetadataProto { 43 | address: Some(m.sender.into()), 44 | sender_device: m.sender_device.try_into().ok(), 45 | timestamp: m.timestamp.try_into().ok(), 46 | server_received_timestamp: None, 47 | server_delivered_timestamp: None, 48 | needs_receipt: Some(m.needs_receipt), 49 | server_guid: None, 50 | group_id: None, 51 | destination_uuid: Some(m.destination.raw_uuid().to_string()), 52 | } 53 | } 54 | } 55 | 56 | impl TryFrom for Metadata { 57 | type Error = SledStoreError; 58 | 59 | fn try_from(metadata: MetadataProto) -> Result { 60 | Ok(Metadata { 61 | sender: metadata.address.ok_or(SledStoreError::NoUuid)?.try_into()?, 62 | destination: ServiceId::Aci( 63 | match metadata.destination_uuid.as_deref() { 64 | Some(value) => value.parse().map_err(|_| SledStoreError::NoUuid), 65 | None => Ok(Uuid::nil()), 66 | }? 67 | .into(), 68 | ), 69 | sender_device: metadata 70 | .sender_device 71 | .and_then(|m| m.try_into().ok()) 72 | .unwrap_or_default(), 73 | server_guid: metadata 74 | .server_guid 75 | .and_then(|u| crate::Uuid::from_str(&u).ok()), 76 | timestamp: metadata 77 | .timestamp 78 | .and_then(|m| m.try_into().ok()) 79 | .unwrap_or_default(), 80 | needs_receipt: metadata.needs_receipt.unwrap_or_default(), 81 | unidentified_sender: false, 82 | was_plaintext: false, 83 | }) 84 | } 85 | } 86 | 87 | #[derive(Clone, PartialEq, ::prost::Message)] 88 | pub struct ContentProto { 89 | #[prost(message, required, tag = "1")] 90 | metadata: MetadataProto, 91 | #[prost(message, required, tag = "2")] 92 | content: proto::Content, 93 | } 94 | 95 | impl From for ContentProto { 96 | fn from(c: Content) -> Self { 97 | (c.metadata, c.body).into() 98 | } 99 | } 100 | 101 | impl From<(Metadata, ContentBody)> for ContentProto { 102 | fn from((metadata, content_body): (Metadata, ContentBody)) -> Self { 103 | ContentProto { 104 | metadata: metadata.into(), 105 | content: content_body.into_proto(), 106 | } 107 | } 108 | } 109 | 110 | impl TryInto for ContentProto { 111 | type Error = SledStoreError; 112 | 113 | fn try_into(self) -> Result { 114 | let metadata = self.metadata.try_into()?; 115 | Content::from_proto(self.content, metadata).map_err(|_| SledStoreError::UnsupportedContent) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /presage-store-sled/src/protobuf/InternalSerialization.proto: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2019 Open Whisper Systems 3 | * 4 | * Licensed according to the LICENSE file in this repository. 5 | */ 6 | syntax = "proto2"; 7 | 8 | package textsecure; 9 | 10 | // Not needed 11 | // import "SignalService.proto"; 12 | 13 | option java_package = "org.whispersystems.signalservice.internal.serialize.protos"; 14 | option java_multiple_files = true; 15 | 16 | // Not needed 17 | // message SignalServiceContentProto { 18 | // optional AddressProto localAddress = 1; 19 | // optional MetadataProto metadata = 2; 20 | // oneof data { 21 | // signalservice.DataMessage legacyDataMessage = 3; 22 | // signalservice.Content content = 4; 23 | // } 24 | // } 25 | 26 | message SignalServiceEnvelopeProto { 27 | optional int32 type = 1; 28 | optional string sourceUuid = 2; 29 | optional string sourceE164 = 3; 30 | optional int32 deviceId = 4; 31 | optional bytes legacyMessage = 5; 32 | optional bytes content = 6; 33 | optional int64 timestamp = 7; 34 | optional int64 serverReceivedTimestamp = 8; 35 | optional int64 serverDeliveredTimestamp = 9; 36 | optional string serverGuid = 10; 37 | optional string destinationUuid = 11; 38 | optional bool urgent = 12 [default = true]; 39 | } 40 | 41 | message MetadataProto { 42 | optional AddressProto address = 1; 43 | optional int32 senderDevice = 2; 44 | optional int64 timestamp = 3; 45 | optional int64 serverReceivedTimestamp = 5; 46 | optional int64 serverDeliveredTimestamp = 6; 47 | optional bool needsReceipt = 4; 48 | optional string serverGuid = 7; 49 | optional bytes groupId = 8; 50 | optional string destinationUuid = 9; 51 | } 52 | 53 | message AddressProto { 54 | optional bytes uuid = 1; 55 | // optional string e164 = 2; 56 | } 57 | -------------------------------------------------------------------------------- /presage-store-sqlite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "presage-store-sqlite" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | presage = { path = "../presage" } 8 | 9 | async-trait = "0.1.83" 10 | bytes = "1.9.0" 11 | chrono = "0.4.38" 12 | prost = "0.13.4" 13 | serde_json = "1.0.135" 14 | sqlx = { version = "0.8.2", features = ["json", "sqlite", "uuid"] } 15 | thiserror = "2.0.0" 16 | tracing = "0.1.41" 17 | uuid = "1.12.0" 18 | -------------------------------------------------------------------------------- /presage-store-sqlite/migrations/20250112201436_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value BLOB NOT NULL); 2 | 3 | -- protocol 4 | CREATE TABLE IF NOT EXISTS sessions ( 5 | address TEXT NOT NULL, 6 | device_id INTEGER NOT NULL, 7 | identity TEXT NOT NULL CHECK (identity IN ('aci', 'pni')), 8 | record BLOB NOT NULL, 9 | PRIMARY KEY (address, device_id, identity) 10 | ); 11 | 12 | CREATE TABLE IF NOT EXISTS identities ( 13 | address TEXT NOT NULL, 14 | device_id INTEGER NOT NULL, 15 | identity TEXT NOT NULL CHECK (identity IN ('aci', 'pni')), 16 | record BLOB NOT NULL, 17 | PRIMARY KEY (address, device_id, identity) 18 | ); 19 | 20 | CREATE TABLE IF NOT EXISTS pre_keys ( 21 | id INTEGER NOT NULL, 22 | identity TEXT NOT NULL CHECK (identity IN ('aci', 'pni')), 23 | record BLOB NOT NULL, 24 | PRIMARY KEY (id, identity) 25 | ); 26 | 27 | CREATE TABLE IF NOT EXISTS signed_pre_keys ( 28 | id INTEGER NOT NULL, 29 | identity TEXT NOT NULL CHECK (identity IN ('aci', 'pni')), 30 | record BLOB NOT NULL, 31 | PRIMARY KEY (id, identity) 32 | ); 33 | 34 | CREATE TABLE IF NOT EXISTS kyber_pre_keys ( 35 | id INTEGER NOT NULL, 36 | identity TEXT NOT NULL CHECK (identity IN ('aci', 'pni')), 37 | record BLOB NOT NULL, 38 | is_last_resort INTEGER NOT NULL DEFAULT 0, 39 | PRIMARY KEY (id, identity) 40 | ); 41 | 42 | CREATE TABLE IF NOT EXISTS sender_keys ( 43 | address TEXT NOT NULL, 44 | device_id INTEGER NOT NULL, 45 | identity TEXT NOT NULL CHECK (identity IN ('aci', 'pni')), 46 | distribution_id TEXT NOT NULL, 47 | record BLOB NOT NULL, 48 | PRIMARY KEY (address, device_id, identity, distribution_id) 49 | ); 50 | 51 | -- content 52 | CREATE TABLE IF NOT EXISTS contacts ( 53 | uuid BLOB NOT NULL PRIMARY KEY, 54 | phone_number TEXT, 55 | name TEXT NOT NULL, 56 | color TEXT, 57 | profile_key BLOB NOT NULL, 58 | expire_timer INTEGER NOT NULL, 59 | expire_timer_version INTEGER NOT NULL DEFAULT 2, 60 | inbox_position INTEGER NOT NULL, 61 | archived BOOLEAN NOT NULL, 62 | avatar BLOB 63 | ); 64 | 65 | CREATE TABLE IF NOT EXISTS contacts_verification_state ( 66 | destination_aci BLOB NOT NULL PRIMARY KEY, 67 | identity_key BLOB NOT NULL, 68 | is_verified BOOLEAN, 69 | FOREIGN KEY (destination_aci) REFERENCES contacts (uuid) ON UPDATE CASCADE 70 | ); 71 | 72 | CREATE TABLE IF NOT EXISTS profile_keys (uuid BLOB NOT NULL PRIMARY KEY, key BLOB NOT NULL); 73 | 74 | CREATE TABLE IF NOT EXISTS profiles ( 75 | uuid BLOB NOT NULL PRIMARY KEY, 76 | given_name TEXT, 77 | family_name TEXT, 78 | about TEXT, 79 | about_emoji TEXT, 80 | avatar TEXT, 81 | unrestricted_unidentified_access BOOLEAN NOT NULL DEFAULT 0, 82 | FOREIGN KEY (uuid) REFERENCES profile_keys (uuid) ON DELETE CASCADE 83 | ); 84 | 85 | CREATE TABLE IF NOT EXISTS profile_avatars ( 86 | uuid BLOB NOT NULL PRIMARY KEY, 87 | bytes BLOB NOT NULL, 88 | FOREIGN KEY (uuid) REFERENCES profile_keys (uuid) ON UPDATE CASCADE 89 | ); 90 | 91 | CREATE TABLE IF NOT EXISTS groups ( 92 | master_key BLOB NOT NULL PRIMARY KEY, 93 | title TEXT NOT NULL, 94 | revision INTEGER NOT NULL DEFAULT 0, 95 | invite_link_password BLOB, 96 | access_control BLOB, 97 | avatar TEXT NOT NULL, 98 | description TEXT, 99 | members BLOB NOT NULL, 100 | pending_members BLOB NOT NULL, 101 | requesting_members BLOB NOT NULL 102 | ); 103 | 104 | CREATE TABLE IF NOT EXISTS group_avatars ( 105 | group_master_key BLOB PRIMARY KEY, 106 | bytes BLOB NOT NULL, 107 | FOREIGN KEY (group_master_key) REFERENCES groups (master_key) ON DELETE CASCADE 108 | ); 109 | 110 | CREATE TABLE IF NOT EXISTS threads ( 111 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 112 | group_master_key BLOB UNIQUE, 113 | recipient_id TEXT UNIQUE 114 | ); 115 | 116 | CREATE TABLE IF NOT EXISTS thread_messages ( 117 | ts INTEGER NOT NULL, 118 | thread_id INTEGER NOT NULL, 119 | sender_service_id TEXT NOT NULL, 120 | sender_device_id INTEGER NOT NULL, 121 | destination_service_id TEXT NOT NULL, 122 | needs_receipt BOOLEAN NOT NULL, 123 | unidentified_sender BOOLEAN NOT NULL, 124 | content_body BLOB NOT NULL, 125 | was_plaintext BOOLEAN NOT NULL, 126 | PRIMARY KEY (ts, thread_id), 127 | FOREIGN KEY (thread_id) REFERENCES threads (id) ON UPDATE CASCADE 128 | ); 129 | 130 | CREATE TABLE IF NOT EXISTS sticker_packs ( 131 | id BLOB PRIMARY KEY NOT NULL, 132 | key BLOB NOT NULL, 133 | manifest BLOB NOT NULL 134 | ); 135 | -------------------------------------------------------------------------------- /presage-store-sqlite/src/content.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Bound, RangeBounds}; 2 | 3 | use presage::{ 4 | AvatarBytes, 5 | libsignal_service::{ 6 | Profile, 7 | content::Metadata, 8 | prelude::{Content, ProfileKey, Uuid}, 9 | zkgroup::GroupMasterKeyBytes, 10 | }, 11 | model::{contacts::Contact, groups::Group}, 12 | proto::{Verified, verified}, 13 | store::{ContentsStore, StickerPack, Thread}, 14 | }; 15 | use sqlx::{query, query_as, query_scalar, types::Json}; 16 | 17 | use crate::{ 18 | SqliteStore, SqliteStoreError, 19 | data::{SqlContact, SqlGroup, SqlMessage, SqlProfile, SqlStickerPack}, 20 | }; 21 | 22 | impl ContentsStore for SqliteStore { 23 | type ContentsStoreError = SqliteStoreError; 24 | 25 | type ContactsIter = Box>>; 26 | 27 | type GroupsIter = 28 | Box>>; 29 | 30 | type MessagesIter = Box>>; 31 | 32 | type StickerPacksIter = Box>>; 33 | 34 | async fn clear_profiles(&mut self) -> Result<(), Self::ContentsStoreError> { 35 | todo!() 36 | } 37 | 38 | async fn clear_contents(&mut self) -> Result<(), Self::ContentsStoreError> { 39 | todo!() 40 | } 41 | 42 | async fn clear_messages(&mut self) -> Result<(), Self::ContentsStoreError> { 43 | todo!() 44 | } 45 | 46 | async fn clear_thread(&mut self, thread: &Thread) -> Result<(), Self::ContentsStoreError> { 47 | let (group_master_key, recipient_id) = thread.unzip(); 48 | query!( 49 | "DELETE FROM thread_messages WHERE thread_id = ( 50 | SELECT id FROM threads WHERE group_master_key = ? OR recipient_id = ?)", 51 | group_master_key, 52 | recipient_id, 53 | ) 54 | .execute(&self.db) 55 | .await?; 56 | Ok(()) 57 | } 58 | 59 | async fn save_message( 60 | &self, 61 | thread: &Thread, 62 | Content { metadata, body }: Content, 63 | ) -> Result<(), Self::ContentsStoreError> { 64 | let mut tx = self.db.begin().await?; 65 | 66 | let thread_id = match thread { 67 | Thread::Contact(uuid) => { 68 | query_scalar!( 69 | "INSERT INTO threads(recipient_id, group_master_key) VALUES (?1, NULL) 70 | ON CONFLICT DO UPDATE SET recipient_id = ?1 RETURNING id", 71 | uuid, 72 | ) 73 | .fetch_one(&mut *tx) 74 | .await? 75 | } 76 | Thread::Group(master_key_bytes) => { 77 | let master_key_bytes = master_key_bytes.as_slice(); 78 | query_scalar!( 79 | "INSERT INTO threads(recipient_id, group_master_key) VALUES (NULL, ?1) 80 | ON CONFLICT DO UPDATE SET group_master_key = ?1 RETURNING id", 81 | master_key_bytes 82 | ) 83 | .fetch_one(&mut *tx) 84 | .await? 85 | } 86 | }; 87 | 88 | let Metadata { 89 | sender, 90 | destination, 91 | sender_device, 92 | timestamp, 93 | needs_receipt, 94 | unidentified_sender, 95 | server_guid: _, 96 | was_plaintext, 97 | } = metadata; 98 | 99 | let sender_service_id = sender.service_id_string(); 100 | let destination_service_id = destination.service_id_string(); 101 | 102 | let proto_bytes = prost::Message::encode_to_vec(&body.into_proto()); 103 | let timestamp: i64 = timestamp 104 | .try_into() 105 | .map_err(|_| SqliteStoreError::InvalidFormat)?; 106 | 107 | query!( 108 | "INSERT OR REPLACE INTO thread_messages ( 109 | ts, 110 | thread_id, 111 | sender_service_id, 112 | sender_device_id, 113 | destination_service_id, 114 | needs_receipt, 115 | unidentified_sender, 116 | content_body, 117 | was_plaintext 118 | ) 119 | VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", 120 | timestamp, 121 | thread_id, 122 | sender_service_id, 123 | sender_device, 124 | destination_service_id, 125 | needs_receipt, 126 | unidentified_sender, 127 | proto_bytes, 128 | was_plaintext, 129 | ) 130 | .execute(&mut *tx) 131 | .await?; 132 | 133 | tx.commit().await?; 134 | Ok(()) 135 | } 136 | 137 | async fn delete_message( 138 | &mut self, 139 | thread: &Thread, 140 | timestamp: u64, 141 | ) -> Result { 142 | let timestamp: i64 = timestamp 143 | .try_into() 144 | .map_err(|_| SqliteStoreError::InvalidFormat)?; 145 | let (group_master_key, recipient_id) = thread.unzip(); 146 | let res = query!( 147 | "DELETE FROM thread_messages 148 | WHERE ts = ? AND thread_id = ( 149 | SELECT id FROM threads WHERE group_master_key = ? OR recipient_id = ?)", 150 | timestamp, 151 | group_master_key, 152 | recipient_id, 153 | ) 154 | .execute(&self.db) 155 | .await?; 156 | Ok(res.rows_affected() > 0) 157 | } 158 | 159 | async fn message( 160 | &self, 161 | thread: &Thread, 162 | timestamp: u64, 163 | ) -> Result, Self::ContentsStoreError> { 164 | let timestamp: i64 = timestamp 165 | .try_into() 166 | .map_err(|_| SqliteStoreError::InvalidFormat)?; 167 | let (group_master_key, recipient_id) = thread.unzip(); 168 | let message = query_as!( 169 | SqlMessage, 170 | r#"SELECT 171 | ts AS "ts: _", 172 | sender_service_id, 173 | sender_device_id AS "sender_device_id: _", 174 | destination_service_id, 175 | needs_receipt, 176 | unidentified_sender, 177 | content_body, 178 | was_plaintext 179 | FROM thread_messages 180 | WHERE ts = ? AND thread_id = ( 181 | SELECT id FROM threads WHERE group_master_key = ? OR recipient_id = ?)"#, 182 | timestamp, 183 | group_master_key, 184 | recipient_id, 185 | ) 186 | .fetch_optional(&self.db) 187 | .await?; 188 | message.map(|m| m.try_into()).transpose() 189 | } 190 | 191 | async fn messages( 192 | &self, 193 | thread: &Thread, 194 | range: impl RangeBounds, 195 | ) -> Result { 196 | let (group_master_key, recipient_id) = thread.unzip(); 197 | 198 | let (start_incl, start_excl) = range.start_bound().into_sql_bound(); 199 | let (end_incl, end_excl) = range.end_bound().into_sql_bound(); 200 | 201 | let rows = query_as!( 202 | SqlMessage, 203 | r#"SELECT 204 | ts AS "ts: _", 205 | sender_service_id, 206 | sender_device_id AS "sender_device_id: _", 207 | destination_service_id, 208 | needs_receipt, 209 | unidentified_sender, 210 | content_body, 211 | was_plaintext 212 | FROM thread_messages 213 | WHERE thread_id = ( 214 | SELECT id FROM threads WHERE group_master_key = ? OR recipient_id = ?) 215 | AND coalesce(ts > ?, ts >= ?, true) 216 | AND coalesce(ts < ?, ts <= ?, true) 217 | ORDER BY ts DESC"#, 218 | group_master_key, 219 | recipient_id, 220 | start_incl, 221 | start_excl, 222 | end_incl, 223 | end_excl 224 | ) 225 | .fetch_all(&self.db) 226 | .await?; 227 | 228 | Ok(Box::new(rows.into_iter().map(TryInto::try_into))) 229 | } 230 | 231 | async fn clear_contacts(&mut self) -> Result<(), Self::ContentsStoreError> { 232 | query!("DELETE FROM contacts").execute(&self.db).await?; 233 | Ok(()) 234 | } 235 | 236 | async fn save_contact(&mut self, contact: &Contact) -> Result<(), Self::ContentsStoreError> { 237 | let profile_key: &[u8] = contact.profile_key.as_ref(); 238 | let avatar_bytes = contact.avatar.as_ref().map(|a| a.reader.to_vec()); 239 | let phone_number = contact.phone_number.as_ref().map(|p| p.to_string()); 240 | 241 | let mut tx = self.db.begin().await?; 242 | 243 | query!( 244 | "INSERT OR REPLACE INTO contacts 245 | VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 246 | contact.uuid, 247 | phone_number, 248 | contact.name, 249 | contact.color, 250 | profile_key, 251 | contact.expire_timer, 252 | contact.expire_timer_version, 253 | contact.inbox_position, 254 | contact.archived, 255 | avatar_bytes, 256 | ) 257 | .execute(&mut *tx) 258 | .await?; 259 | 260 | let Verified { 261 | destination_aci, 262 | identity_key, 263 | state, 264 | .. 265 | } = &contact.verified; 266 | let is_verified = match verified::State::try_from(state.unwrap_or_default()) { 267 | Err(_) | Ok(verified::State::Default) => None, 268 | Ok(verified::State::Unverified) => Some(false), 269 | Ok(verified::State::Verified) => Some(true), 270 | }; 271 | 272 | if let Some((destination_aci, identity_key)) = 273 | destination_aci.as_ref().zip(identity_key.as_ref()) 274 | { 275 | query!( 276 | "INSERT OR REPLACE INTO contacts_verification_state( 277 | destination_aci, identity_key, is_verified 278 | ) VALUES(?, ?, ?)", 279 | destination_aci, 280 | identity_key, 281 | is_verified, 282 | ) 283 | .execute(&mut *tx) 284 | .await?; 285 | } 286 | 287 | tx.commit().await?; 288 | 289 | Ok(()) 290 | } 291 | 292 | async fn contacts(&self) -> Result { 293 | let sql_contacts = query_as!( 294 | SqlContact, 295 | r#"SELECT 296 | uuid AS "uuid: _", 297 | phone_number, 298 | name, 299 | color, 300 | profile_key, 301 | expire_timer, 302 | expire_timer_version, 303 | inbox_position, 304 | archived, 305 | avatar, 306 | destination_aci AS "destination_aci: _", 307 | identity_key, 308 | is_verified 309 | FROM contacts c 310 | LEFT JOIN contacts_verification_state cv ON c.uuid = cv.destination_aci 311 | ORDER BY c.inbox_position"# 312 | ) 313 | .fetch_all(&self.db) 314 | .await?; 315 | Ok(Box::new(sql_contacts.into_iter().map(TryInto::try_into))) 316 | } 317 | 318 | async fn contact_by_id(&self, id: &Uuid) -> Result, Self::ContentsStoreError> { 319 | query_as!( 320 | SqlContact, 321 | r#"SELECT 322 | uuid AS "uuid: _", 323 | phone_number, 324 | name, 325 | color, 326 | profile_key, 327 | expire_timer, 328 | expire_timer_version, 329 | inbox_position, 330 | archived, 331 | avatar, 332 | destination_aci AS "destination_aci: _", 333 | identity_key, 334 | is_verified 335 | FROM contacts c 336 | LEFT JOIN contacts_verification_state cv ON c.uuid = cv.destination_aci 337 | WHERE c.uuid = ?"#, 338 | id 339 | ) 340 | .fetch_optional(&self.db) 341 | .await? 342 | .map(TryInto::try_into) 343 | .transpose() 344 | } 345 | 346 | async fn clear_groups(&mut self) -> Result<(), Self::ContentsStoreError> { 347 | query!("DELETE FROM groups").execute(&self.db).await?; 348 | Ok(()) 349 | } 350 | 351 | async fn save_group( 352 | &self, 353 | master_key: GroupMasterKeyBytes, 354 | group: impl Into, 355 | ) -> Result<(), Self::ContentsStoreError> { 356 | let g = SqlGroup::from_group(&master_key, group.into()); 357 | let master_key = g.master_key.as_ref(); 358 | query!( 359 | "INSERT OR REPLACE INTO groups VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 360 | master_key, 361 | g.title, 362 | g.revision, 363 | g.invite_link_password, 364 | g.access_control, 365 | g.avatar, 366 | g.description, 367 | g.members, 368 | g.pending_members, 369 | g.requesting_members, 370 | ) 371 | .execute(&self.db) 372 | .await?; 373 | Ok(()) 374 | } 375 | 376 | async fn groups(&self) -> Result { 377 | let sql_groups = query_as!( 378 | SqlGroup, 379 | r#"SELECT 380 | master_key, 381 | title, 382 | revision AS "revision: _", 383 | invite_link_password, 384 | access_control AS "access_control: _", 385 | avatar, 386 | description, 387 | members AS "members: _", 388 | pending_members AS "pending_members: _", 389 | requesting_members AS "requesting_members: _" 390 | FROM groups"#, 391 | ) 392 | .fetch_all(&self.db) 393 | .await?; 394 | Ok(Box::new(sql_groups.into_iter().map(SqlGroup::into_group))) 395 | } 396 | 397 | async fn group( 398 | &self, 399 | master_key: GroupMasterKeyBytes, 400 | ) -> Result, Self::ContentsStoreError> { 401 | let master_key_bytes = master_key.as_slice(); 402 | query_as!( 403 | SqlGroup, 404 | r#"SELECT 405 | master_key, 406 | title, 407 | revision AS "revision: _", 408 | invite_link_password, 409 | access_control AS "access_control: _", 410 | avatar, 411 | description, 412 | members AS "members: _", 413 | pending_members AS "pending_members: _", 414 | requesting_members AS "requesting_members: _" 415 | FROM groups 416 | WHERE master_key = ? 417 | LIMIT 1"#, 418 | master_key_bytes, 419 | ) 420 | .fetch_optional(&self.db) 421 | .await? 422 | .map(|g| g.into_group().map(|(_master_key, group)| group)) 423 | .transpose() 424 | } 425 | 426 | async fn save_group_avatar( 427 | &self, 428 | master_key: GroupMasterKeyBytes, 429 | avatar: &AvatarBytes, 430 | ) -> Result<(), Self::ContentsStoreError> { 431 | let master_key_bytes = master_key.as_slice(); 432 | query!( 433 | "UPDATE group_avatars SET bytes = ? WHERE group_master_key = ?", 434 | avatar, 435 | master_key_bytes, 436 | ) 437 | .execute(&self.db) 438 | .await?; 439 | Ok(()) 440 | } 441 | 442 | async fn group_avatar( 443 | &self, 444 | master_key: GroupMasterKeyBytes, 445 | ) -> Result, Self::ContentsStoreError> { 446 | let master_key_bytes = master_key.as_slice(); 447 | query_scalar!( 448 | "SELECT bytes FROM group_avatars WHERE group_master_key = ?", 449 | master_key_bytes, 450 | ) 451 | .fetch_optional(&self.db) 452 | .await 453 | .map_err(From::from) 454 | } 455 | 456 | async fn upsert_profile_key( 457 | &mut self, 458 | uuid: &Uuid, 459 | key: ProfileKey, 460 | ) -> Result { 461 | let profile_key_bytes = key.bytes.as_slice(); 462 | let res = query_scalar!( 463 | "INSERT OR REPLACE INTO profile_keys (uuid, key) VALUES (?, ?)", 464 | uuid, 465 | profile_key_bytes 466 | ) 467 | .execute(&self.db) 468 | .await?; 469 | Ok(res.rows_affected() == 0) 470 | } 471 | 472 | async fn profile_key( 473 | &self, 474 | uuid: &Uuid, 475 | ) -> Result, Self::ContentsStoreError> { 476 | let profile_key = query_scalar!("SELECT key FROM profile_keys WHERE uuid = ?", uuid) 477 | .fetch_optional(&self.db) 478 | .await? 479 | .and_then(|bytes| bytes.try_into().ok().map(ProfileKey::create)); 480 | Ok(profile_key) 481 | } 482 | 483 | async fn save_profile( 484 | &mut self, 485 | uuid: Uuid, 486 | key: ProfileKey, 487 | profile: Profile, 488 | ) -> Result<(), Self::ContentsStoreError> { 489 | self.upsert_profile_key(&uuid, key).await?; 490 | let Profile { 491 | name, 492 | about, 493 | about_emoji, 494 | avatar, 495 | unrestricted_unidentified_access, 496 | } = profile; 497 | let (given_name, family_name) = name.map(|n| (n.given_name, n.family_name)).unzip(); 498 | let family_name = family_name.flatten(); 499 | query!( 500 | "INSERT OR REPLACE INTO profiles VALUES (?, ?, ?, ?, ?, ?, ?)", 501 | uuid, 502 | given_name, 503 | family_name, 504 | about, 505 | about_emoji, 506 | avatar, 507 | unrestricted_unidentified_access, 508 | ) 509 | .execute(&self.db) 510 | .await?; 511 | Ok(()) 512 | } 513 | 514 | async fn profile( 515 | &self, 516 | uuid: Uuid, 517 | key: ProfileKey, 518 | ) -> Result, Self::ContentsStoreError> { 519 | let profile_key_bytes = key.bytes.as_slice(); 520 | let profile = query_as!( 521 | SqlProfile, 522 | "SELECT 523 | p.given_name, 524 | p.family_name, 525 | p.about, 526 | p.about_emoji, 527 | p.avatar, 528 | p.unrestricted_unidentified_access 529 | FROM profile_keys pk 530 | INNER JOIN profiles p ON p.uuid = pk.uuid 531 | WHERE pk.uuid = ? AND pk.key = ?", 532 | uuid, 533 | profile_key_bytes, 534 | ) 535 | .fetch_optional(&self.db) 536 | .await?; 537 | Ok(profile.map(|p| p.into())) 538 | } 539 | 540 | async fn save_profile_avatar( 541 | &mut self, 542 | uuid: Uuid, 543 | _key: ProfileKey, 544 | profile: &AvatarBytes, 545 | ) -> Result<(), Self::ContentsStoreError> { 546 | query!( 547 | "UPDATE profile_avatars SET bytes = ? WHERE uuid = ?", 548 | profile, 549 | uuid, 550 | ) 551 | .execute(&self.db) 552 | .await?; 553 | Ok(()) 554 | } 555 | 556 | async fn profile_avatar( 557 | &self, 558 | uuid: Uuid, 559 | _key: ProfileKey, 560 | ) -> Result, Self::ContentsStoreError> { 561 | query_scalar!("SELECT bytes FROM profile_avatars WHERE uuid = ?", uuid) 562 | .fetch_optional(&self.db) 563 | .await 564 | .map_err(From::from) 565 | } 566 | 567 | async fn add_sticker_pack( 568 | &mut self, 569 | pack: &StickerPack, 570 | ) -> Result<(), Self::ContentsStoreError> { 571 | let manifest_json = Json(&pack.manifest); 572 | query!( 573 | "INSERT OR REPLACE INTO sticker_packs(id, key, manifest) VALUES(?, ?, ?)", 574 | pack.id, 575 | pack.key, 576 | manifest_json, 577 | ) 578 | .execute(&self.db) 579 | .await?; 580 | Ok(()) 581 | } 582 | 583 | async fn sticker_pack( 584 | &self, 585 | id: &[u8], 586 | ) -> Result, Self::ContentsStoreError> { 587 | let pack = query_as!( 588 | SqlStickerPack, 589 | r#"SELECT id, key, manifest AS "manifest: _" FROM sticker_packs WHERE id = ?"#, 590 | id 591 | ) 592 | .fetch_optional(&self.db) 593 | .await?; 594 | Ok(pack.map(From::from)) 595 | } 596 | 597 | async fn remove_sticker_pack(&mut self, id: &[u8]) -> Result { 598 | let res = query!("DELETE FROM sticker_packs WHERE id = ?", id) 599 | .execute(&self.db) 600 | .await?; 601 | Ok(res.rows_affected() > 0) 602 | } 603 | 604 | async fn sticker_packs(&self) -> Result { 605 | let sql_packs = query_as!( 606 | SqlStickerPack, 607 | r#"SELECT id, key, manifest AS "manifest: _" FROM sticker_packs"#, 608 | ) 609 | .fetch_all(&self.db) 610 | .await?; 611 | Ok(Box::new(sql_packs.into_iter().map(|pack| Ok(pack.into())))) 612 | } 613 | } 614 | 615 | trait ThreadExt { 616 | fn group_master_key(&self) -> Option<&[u8]>; 617 | fn recipient_id(&self) -> Option; 618 | 619 | fn unzip(&self) -> (Option<&[u8]>, Option) { 620 | (self.group_master_key(), self.recipient_id()) 621 | } 622 | } 623 | 624 | impl ThreadExt for Thread { 625 | fn group_master_key(&self) -> Option<&[u8]> { 626 | match self { 627 | Thread::Contact(_) => None, 628 | Thread::Group(master_key) => Some(master_key.as_slice()), 629 | } 630 | } 631 | 632 | fn recipient_id(&self) -> Option { 633 | match self { 634 | Thread::Contact(uuid) => Some(*uuid), 635 | Thread::Group(_) => None, 636 | } 637 | } 638 | } 639 | 640 | trait BoundExt { 641 | fn into_sql_bound(self) -> (Option, Option); 642 | } 643 | 644 | impl BoundExt for Bound<&u64> { 645 | fn into_sql_bound(self) -> (Option, Option) { 646 | match self { 647 | Bound::Excluded(x) => (Some(*x as i64), None), 648 | Bound::Included(x) => (None, Some(*x as i64)), 649 | Bound::Unbounded => (None, None), 650 | } 651 | } 652 | } 653 | -------------------------------------------------------------------------------- /presage-store-sqlite/src/data.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use bytes::Bytes; 4 | use presage::{ 5 | libsignal_service::{ 6 | Profile, 7 | content::Metadata, 8 | models::Attachment, 9 | prelude::{AccessControl, Content, Member, phonenumber}, 10 | profile_name::ProfileName, 11 | protocol::ServiceId, 12 | zkgroup::GroupMasterKeyBytes, 13 | }, 14 | model::{ 15 | contacts::Contact, 16 | groups::{Group, PendingMember, RequestingMember}, 17 | }, 18 | proto::{self, Verified, verified}, 19 | store::{StickerPack, StickerPackManifest}, 20 | }; 21 | use sqlx::types::Json; 22 | use uuid::Uuid; 23 | 24 | use crate::SqliteStoreError; 25 | 26 | #[derive(Debug)] 27 | pub struct SqlContact { 28 | pub uuid: Uuid, 29 | pub phone_number: Option, 30 | pub name: String, 31 | pub color: Option, 32 | pub profile_key: Vec, 33 | pub expire_timer: i64, 34 | pub expire_timer_version: i64, 35 | pub inbox_position: i64, 36 | pub archived: bool, 37 | pub avatar: Option>, 38 | 39 | pub destination_aci: Option, 40 | pub identity_key: Option>, 41 | pub is_verified: Option, 42 | } 43 | 44 | impl TryInto for SqlContact { 45 | type Error = SqliteStoreError; 46 | 47 | #[tracing::instrument] 48 | fn try_into(self) -> Result { 49 | Ok(Contact { 50 | uuid: self.uuid, 51 | phone_number: self 52 | .phone_number 53 | .map(|p| phonenumber::parse(None, &p)) 54 | .transpose()?, 55 | name: self.name, 56 | color: self.color, 57 | verified: Verified { 58 | destination_aci: self.destination_aci, 59 | identity_key: self.identity_key, 60 | state: self.is_verified.map(|v| { 61 | match v { 62 | true => verified::State::Verified, 63 | false => verified::State::Unverified, 64 | } 65 | .into() 66 | }), 67 | null_message: None, 68 | }, 69 | profile_key: self.profile_key, 70 | expire_timer: self.expire_timer as u32, 71 | expire_timer_version: self.expire_timer_version as u32, 72 | inbox_position: self.inbox_position as u32, 73 | archived: self.archived, 74 | avatar: self.avatar.map(|b| Attachment { 75 | content_type: "application/octet-stream".to_owned(), 76 | reader: Bytes::from(b), 77 | }), 78 | }) 79 | } 80 | } 81 | 82 | #[derive(Debug)] 83 | pub struct SqlProfile { 84 | pub given_name: Option, 85 | pub family_name: Option, 86 | pub about: Option, 87 | pub about_emoji: Option, 88 | pub avatar: Option, 89 | pub unrestricted_unidentified_access: bool, 90 | } 91 | 92 | impl From for Profile { 93 | fn from( 94 | SqlProfile { 95 | given_name, 96 | family_name, 97 | about, 98 | about_emoji, 99 | avatar, 100 | unrestricted_unidentified_access, 101 | }: SqlProfile, 102 | ) -> Self { 103 | Profile { 104 | name: given_name.map(|gn| ProfileName { 105 | given_name: gn, 106 | family_name, 107 | }), 108 | about, 109 | about_emoji, 110 | avatar, 111 | unrestricted_unidentified_access, 112 | } 113 | } 114 | } 115 | 116 | #[derive(Debug)] 117 | pub(crate) struct SqlGroup<'a> { 118 | pub(crate) master_key: Cow<'a, [u8]>, 119 | pub(crate) title: String, 120 | pub(crate) revision: u32, 121 | pub(crate) invite_link_password: Option>, 122 | pub(crate) access_control: Option>, 123 | pub(crate) avatar: String, 124 | pub(crate) description: Option, 125 | pub(crate) members: Json>, 126 | pub(crate) pending_members: Json>, 127 | pub(crate) requesting_members: Json>, 128 | } 129 | 130 | impl SqlGroup<'_> { 131 | #[tracing::instrument] 132 | pub fn from_group(master_key: &GroupMasterKeyBytes, group: Group) -> SqlGroup { 133 | SqlGroup { 134 | master_key: Cow::Borrowed(master_key.as_slice()), 135 | title: group.title, 136 | revision: group.revision, 137 | invite_link_password: Some(group.invite_link_password), 138 | access_control: group.access_control.map(Json), 139 | avatar: group.avatar, 140 | description: group.description, 141 | members: Json(group.members), 142 | pending_members: Json(group.pending_members), 143 | requesting_members: Json(group.requesting_members), 144 | } 145 | } 146 | 147 | #[tracing::instrument] 148 | pub fn into_group(self) -> Result<(GroupMasterKeyBytes, Group), SqliteStoreError> { 149 | let Self { 150 | master_key, 151 | title, 152 | revision, 153 | invite_link_password, 154 | access_control, 155 | avatar, 156 | description, 157 | members: Json(members), 158 | pending_members: Json(pending_members), 159 | requesting_members: Json(requesting_members), 160 | } = self; 161 | let master_key = master_key 162 | .as_ref() 163 | .try_into() 164 | .map_err(|_| SqliteStoreError::InvalidFormat)?; 165 | let access_control = access_control.map(|Json(x)| x); 166 | let group = Group { 167 | title, 168 | avatar, 169 | disappearing_messages_timer: None, 170 | access_control, 171 | revision, 172 | members, 173 | pending_members, 174 | requesting_members, 175 | invite_link_password: invite_link_password.unwrap_or_default(), 176 | description, 177 | }; 178 | Ok((master_key, group)) 179 | } 180 | } 181 | 182 | #[derive(Debug)] 183 | pub struct SqlMessage { 184 | pub ts: u64, 185 | 186 | pub sender_service_id: String, 187 | pub sender_device_id: u32, 188 | pub destination_service_id: String, 189 | pub needs_receipt: bool, 190 | pub unidentified_sender: bool, 191 | 192 | pub content_body: Vec, 193 | pub was_plaintext: bool, 194 | } 195 | 196 | impl TryInto for SqlMessage { 197 | type Error = SqliteStoreError; 198 | 199 | #[tracing::instrument] 200 | fn try_into(self) -> Result { 201 | let Self { 202 | ts, 203 | sender_service_id, 204 | sender_device_id, 205 | destination_service_id, 206 | needs_receipt, 207 | unidentified_sender, 208 | content_body, 209 | was_plaintext, 210 | } = self; 211 | let body: proto::Content = 212 | prost::Message::decode(&*content_body).map_err(|_| SqliteStoreError::InvalidFormat)?; 213 | let sender = ServiceId::parse_from_service_id_string(&sender_service_id) 214 | .ok_or_else(|| SqliteStoreError::InvalidFormat)?; 215 | let destination = ServiceId::parse_from_service_id_string(&destination_service_id) 216 | .ok_or_else(|| SqliteStoreError::InvalidFormat)?; 217 | let metadata = Metadata { 218 | sender, 219 | destination, 220 | sender_device: sender_device_id, 221 | timestamp: ts, 222 | needs_receipt, 223 | unidentified_sender, 224 | server_guid: None, 225 | was_plaintext, 226 | }; 227 | Content::from_proto(body, metadata).map_err(|_| SqliteStoreError::InvalidFormat) 228 | } 229 | } 230 | 231 | pub(crate) struct SqlStickerPack { 232 | pub(crate) id: Vec, 233 | pub(crate) key: Vec, 234 | pub(crate) manifest: Json, 235 | } 236 | 237 | impl From for StickerPack { 238 | fn from( 239 | SqlStickerPack { 240 | id, 241 | key, 242 | manifest: Json(manifest), 243 | }: SqlStickerPack, 244 | ) -> Self { 245 | StickerPack { id, key, manifest } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /presage-store-sqlite/src/error.rs: -------------------------------------------------------------------------------- 1 | use presage::{ 2 | libsignal_service::{prelude::phonenumber, protocol::SignalProtocolError}, 3 | store::StoreError, 4 | }; 5 | use tracing::error; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | #[non_exhaustive] 9 | pub enum SqliteStoreError { 10 | #[error(transparent)] 11 | Db(#[from] sqlx::Error), 12 | #[error(transparent)] 13 | Migrate(#[from] sqlx::migrate::MigrateError), 14 | #[error(transparent)] 15 | Json(#[from] serde_json::Error), 16 | #[error(transparent)] 17 | Uuid(#[from] uuid::Error), 18 | #[error(transparent)] 19 | PhoneNumber(#[from] phonenumber::ParseError), 20 | #[error("conversation error")] 21 | InvalidFormat, 22 | #[error(transparent)] 23 | Protocol(#[from] SignalProtocolError), 24 | } 25 | 26 | impl StoreError for SqliteStoreError {} 27 | 28 | impl From for presage::libsignal_service::protocol::SignalProtocolError { 29 | fn from(error: SqliteStoreError) -> Self { 30 | error!(%error, "presage sqlite store error"); 31 | Self::InvalidState("presage sqlite store error", error.to_string()) 32 | } 33 | } 34 | 35 | pub(crate) trait SqlxErrorExt { 36 | fn into_protocol_error(self) -> Result; 37 | } 38 | 39 | impl SqlxErrorExt for Result { 40 | fn into_protocol_error(self) -> Result { 41 | self.map_err(|error| SignalProtocolError::InvalidState("sqlite", error.to_string())) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /presage-store-sqlite/src/lib.rs: -------------------------------------------------------------------------------- 1 | use presage::{ 2 | libsignal_service::protocol::SenderCertificate, 3 | store::{StateStore, Store}, 4 | }; 5 | use protocol::{IdentityType, SqliteProtocolStore}; 6 | use sqlx::{SqlitePool, query, query_scalar}; 7 | 8 | mod content; 9 | mod data; 10 | mod error; 11 | mod protocol; 12 | 13 | pub use error::SqliteStoreError; 14 | pub use presage::model::identity::OnNewIdentity; 15 | pub use sqlx::sqlite::SqliteConnectOptions; 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct SqliteStore { 19 | pub(crate) db: SqlitePool, 20 | pub(crate) trust_new_identities: OnNewIdentity, 21 | } 22 | 23 | impl SqliteStore { 24 | pub async fn open( 25 | url: &str, 26 | trust_new_identities: OnNewIdentity, 27 | ) -> Result { 28 | let options: SqliteConnectOptions = url.parse()?; 29 | Self::open_with_options(options, trust_new_identities).await 30 | } 31 | 32 | pub async fn open_with_passphrase( 33 | url: &str, 34 | passphrase: Option<&str>, 35 | trust_new_identities: OnNewIdentity, 36 | ) -> Result { 37 | let options: SqliteConnectOptions = url.parse()?; 38 | let options = options.create_if_missing(true).foreign_keys(true); 39 | let options = if let Some(passphrase) = passphrase { 40 | let passphrase = passphrase.replace("'", "''"); 41 | options.pragma("key", format!("'{passphrase}'")) 42 | } else { 43 | options 44 | }; 45 | Self::open_with_options(options, trust_new_identities).await 46 | } 47 | 48 | pub async fn open_with_options( 49 | options: SqliteConnectOptions, 50 | trust_new_identities: OnNewIdentity, 51 | ) -> Result { 52 | let db = SqlitePool::connect_with(options).await?; 53 | sqlx::migrate!().run(&db).await?; 54 | Ok(Self { 55 | db, 56 | trust_new_identities, 57 | }) 58 | } 59 | } 60 | 61 | impl Store for SqliteStore { 62 | type Error = SqliteStoreError; 63 | 64 | type AciStore = SqliteProtocolStore; 65 | 66 | type PniStore = SqliteProtocolStore; 67 | 68 | async fn clear(&mut self) -> Result<(), SqliteStoreError> { 69 | query!("DELETE FROM kv").execute(&self.db).await?; 70 | Ok(()) 71 | } 72 | 73 | fn aci_protocol_store(&self) -> Self::AciStore { 74 | SqliteProtocolStore { 75 | store: self.clone(), 76 | identity: IdentityType::Aci, 77 | } 78 | } 79 | 80 | fn pni_protocol_store(&self) -> Self::PniStore { 81 | SqliteProtocolStore { 82 | store: self.clone(), 83 | identity: IdentityType::Pni, 84 | } 85 | } 86 | } 87 | 88 | impl StateStore for SqliteStore { 89 | type StateStoreError = SqliteStoreError; 90 | 91 | async fn load_registration_data( 92 | &self, 93 | ) -> Result, Self::StateStoreError> { 94 | query_scalar!("SELECT value FROM kv WHERE key = 'registration'") 95 | .fetch_optional(&self.db) 96 | .await? 97 | .map(|value| serde_json::from_slice(&value)) 98 | .transpose() 99 | .map_err(From::from) 100 | } 101 | 102 | async fn save_registration_data( 103 | &mut self, 104 | state: &presage::manager::RegistrationData, 105 | ) -> Result<(), Self::StateStoreError> { 106 | let value = serde_json::to_string(state)?; 107 | query!( 108 | "INSERT OR REPLACE INTO kv (key, value) VALUES ('registration', ?)", 109 | value 110 | ) 111 | .execute(&self.db) 112 | .await?; 113 | Ok(()) 114 | } 115 | 116 | async fn is_registered(&self) -> bool { 117 | self.load_registration_data().await.ok().flatten().is_some() 118 | } 119 | 120 | async fn clear_registration(&mut self) -> Result<(), Self::StateStoreError> { 121 | query!("DELETE FROM kv WHERE key = 'registration'") 122 | .execute(&self.db) 123 | .await?; 124 | Ok(()) 125 | } 126 | 127 | async fn set_aci_identity_key_pair( 128 | &self, 129 | key_pair: presage::libsignal_service::protocol::IdentityKeyPair, 130 | ) -> Result<(), Self::StateStoreError> { 131 | let key = IdentityType::Aci.identity_key_pair_key(); 132 | let value = key_pair.serialize(); 133 | query!( 134 | "INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)", 135 | key, 136 | value 137 | ) 138 | .execute(&self.db) 139 | .await?; 140 | Ok(()) 141 | } 142 | 143 | async fn set_pni_identity_key_pair( 144 | &self, 145 | key_pair: presage::libsignal_service::protocol::IdentityKeyPair, 146 | ) -> Result<(), Self::StateStoreError> { 147 | let key = IdentityType::Pni.identity_key_pair_key(); 148 | let value = key_pair.serialize(); 149 | query!( 150 | "INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)", 151 | key, 152 | value 153 | ) 154 | .execute(&self.db) 155 | .await?; 156 | Ok(()) 157 | } 158 | 159 | async fn sender_certificate(&self) -> Result, Self::StateStoreError> { 160 | query_scalar!("SELECT value FROM kv WHERE key = 'sender_certificate' LIMIT 1") 161 | .fetch_optional(&self.db) 162 | .await? 163 | .map(|value| SenderCertificate::deserialize(&value)) 164 | .transpose() 165 | .map_err(From::from) 166 | } 167 | 168 | async fn save_sender_certificate( 169 | &self, 170 | certificate: &SenderCertificate, 171 | ) -> Result<(), Self::StateStoreError> { 172 | let value = certificate.serialized()?; 173 | query!( 174 | "INSERT OR REPLACE INTO kv (key, value) VALUES ('sender_certificate', ?)", 175 | value 176 | ) 177 | .execute(&self.db) 178 | .await?; 179 | Ok(()) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /presage-store-sqlite/src/protocol.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use chrono::{DateTime, Utc}; 3 | use presage::{ 4 | libsignal_service::{ 5 | pre_keys::{KyberPreKeyStoreExt, PreKeysStore}, 6 | prelude::{IdentityKeyStore, SessionStoreExt, Uuid}, 7 | protocol::{ 8 | Direction, GenericSignedPreKey, IdentityKey, IdentityKeyPair, KyberPreKeyId, 9 | KyberPreKeyRecord, KyberPreKeyStore, PreKeyId, PreKeyRecord, PreKeyStore, 10 | ProtocolAddress, ProtocolStore, SenderKeyRecord, SenderKeyStore, ServiceId, 11 | SessionRecord, SessionStore, SignalProtocolError, SignedPreKeyId, SignedPreKeyRecord, 12 | SignedPreKeyStore, 13 | }, 14 | push_service::DEFAULT_DEVICE_ID, 15 | }, 16 | model::identity::OnNewIdentity, 17 | store::StateStore, 18 | }; 19 | use sqlx::{query, query_scalar}; 20 | use tracing::warn; 21 | 22 | use crate::{SqliteStore, SqliteStoreError, error::SqlxErrorExt}; 23 | 24 | #[derive(Clone)] 25 | pub struct SqliteProtocolStore { 26 | pub(crate) store: SqliteStore, 27 | pub(crate) identity: IdentityType, 28 | } 29 | 30 | #[derive(Debug, Clone, Copy, sqlx::Type)] 31 | #[sqlx(rename_all = "lowercase")] 32 | pub(crate) enum IdentityType { 33 | Aci, 34 | Pni, 35 | } 36 | 37 | impl IdentityType { 38 | pub(crate) fn identity_key_pair_key(&self) -> &'static str { 39 | match self { 40 | Self::Aci => "identity_keypair_aci", 41 | Self::Pni => "identity_keypair_pni", 42 | } 43 | } 44 | } 45 | 46 | impl ProtocolStore for SqliteProtocolStore {} 47 | 48 | #[async_trait(?Send)] 49 | impl SessionStore for SqliteProtocolStore { 50 | /// Look up the session corresponding to `address`. 51 | async fn load_session( 52 | &self, 53 | address: &ProtocolAddress, 54 | ) -> Result, SignalProtocolError> { 55 | let device_id: u32 = address.device_id().into(); 56 | let address = address.name(); 57 | query!( 58 | "SELECT record FROM sessions 59 | WHERE address = ? AND device_id = ? AND identity = ?", 60 | address, 61 | device_id, 62 | self.identity, 63 | ) 64 | .fetch_optional(&self.store.db) 65 | .await 66 | .into_protocol_error()? 67 | .map(|record| SessionRecord::deserialize(&record.record)) 68 | .transpose() 69 | } 70 | 71 | /// Set the entry for `address` to the value of `record`. 72 | async fn store_session( 73 | &mut self, 74 | address: &ProtocolAddress, 75 | record: &SessionRecord, 76 | ) -> Result<(), SignalProtocolError> { 77 | let device_id: u32 = address.device_id().into(); 78 | let address = address.name(); 79 | let record = record.serialize()?; 80 | 81 | let mut transaction = self.store.db.begin().await.into_protocol_error()?; 82 | 83 | // Note: It is faster to do the update in a separate query and only insert the record if 84 | // the update did not do anything. 85 | let res = query!( 86 | "UPDATE sessions SET record = ?4 87 | WHERE address = ?1 AND device_id = ?2 AND identity = ?3", 88 | address, 89 | device_id, 90 | self.identity, 91 | record, 92 | ) 93 | .execute(&mut *transaction) 94 | .await 95 | .into_protocol_error()?; 96 | 97 | if res.rows_affected() == 0 { 98 | query!( 99 | "INSERT INTO sessions (address, device_id, identity, record) 100 | VALUES (?1, ?2, ?3, ?4)", 101 | address, 102 | device_id, 103 | self.identity, 104 | record, 105 | ) 106 | .execute(&mut *transaction) 107 | .await 108 | .into_protocol_error()?; 109 | } 110 | 111 | transaction.commit().await.into_protocol_error()?; 112 | 113 | Ok(()) 114 | } 115 | } 116 | 117 | #[async_trait(?Send)] 118 | impl SessionStoreExt for SqliteProtocolStore { 119 | /// Get the IDs of all known sub devices with active sessions for a recipient. 120 | /// 121 | /// This should return every device except for the main device [DEFAULT_DEVICE_ID]. 122 | async fn get_sub_device_sessions( 123 | &self, 124 | name: &ServiceId, 125 | ) -> Result, SignalProtocolError> { 126 | let address = name.raw_uuid().to_string(); 127 | query_scalar!( 128 | "SELECT device_id AS 'id: u32' FROM sessions 129 | WHERE address = ? AND device_id != ? AND identity = ?", 130 | address, 131 | DEFAULT_DEVICE_ID, 132 | self.identity, 133 | ) 134 | .fetch_all(&self.store.db) 135 | .await 136 | .into_protocol_error() 137 | } 138 | 139 | /// Remove a session record for a recipient ID + device ID tuple. 140 | async fn delete_session(&self, address: &ProtocolAddress) -> Result<(), SignalProtocolError> { 141 | let device_id: u32 = address.device_id().into(); 142 | let address = address.name(); 143 | query!( 144 | "DELETE FROM sessions WHERE address = ? AND device_id = ? AND identity = ?", 145 | address, 146 | device_id, 147 | self.identity, 148 | ) 149 | .execute(&self.store.db) 150 | .await 151 | .into_protocol_error()?; 152 | Ok(()) 153 | } 154 | 155 | /// Remove the session records corresponding to all devices of a recipient 156 | /// ID. 157 | /// 158 | /// Returns the number of deleted sessions. 159 | async fn delete_all_sessions(&self, name: &ServiceId) -> Result { 160 | let address = name.raw_uuid(); 161 | let res = query!( 162 | "DELETE FROM sessions WHERE address = ? AND identity = ?", 163 | address, 164 | self.identity 165 | ) 166 | .execute(&self.store.db) 167 | .await 168 | .map_err(SqliteStoreError::from)?; 169 | Ok(res.rows_affected().try_into().expect("usize overflow")) 170 | } 171 | } 172 | 173 | #[async_trait(?Send)] 174 | impl PreKeyStore for SqliteProtocolStore { 175 | /// Look up the pre-key corresponding to `prekey_id`. 176 | async fn get_pre_key(&self, prekey_id: PreKeyId) -> Result { 177 | let id: u32 = prekey_id.into(); 178 | let record = query_scalar!( 179 | "SELECT record FROM pre_keys WHERE id = ? AND identity = ?", 180 | id, 181 | self.identity 182 | ) 183 | .fetch_one(&self.store.db) 184 | .await 185 | .into_protocol_error()?; 186 | PreKeyRecord::deserialize(&record) 187 | } 188 | 189 | /// Set the entry for `prekey_id` to the value of `record`. 190 | async fn save_pre_key( 191 | &mut self, 192 | prekey_id: PreKeyId, 193 | record: &PreKeyRecord, 194 | ) -> Result<(), SignalProtocolError> { 195 | let id: u32 = prekey_id.into(); 196 | let record = record.serialize()?; 197 | query!( 198 | "INSERT INTO pre_keys (id, identity, record) 199 | VALUES (?1, ?2, ?3) 200 | ON CONFLICT DO UPDATE SET record = ?3", 201 | id, 202 | self.identity, 203 | record, 204 | ) 205 | .execute(&self.store.db) 206 | .await 207 | .into_protocol_error()?; 208 | Ok(()) 209 | } 210 | 211 | /// Remove the entry for `prekey_id`. 212 | async fn remove_pre_key(&mut self, prekey_id: PreKeyId) -> Result<(), SignalProtocolError> { 213 | let id: u32 = prekey_id.into(); 214 | query!( 215 | "DELETE FROM pre_keys WHERE id = ? AND identity = ?", 216 | id, 217 | self.identity 218 | ) 219 | .execute(&self.store.db) 220 | .await 221 | .into_protocol_error()?; 222 | Ok(()) 223 | } 224 | } 225 | 226 | #[async_trait(?Send)] 227 | impl PreKeysStore for SqliteProtocolStore { 228 | /// ID of the next pre key 229 | async fn next_pre_key_id(&self) -> Result { 230 | let max_id = query_scalar!( 231 | "SELECT MAX(id) AS 'id: u32' FROM pre_keys WHERE identity = ?", 232 | self.identity, 233 | ) 234 | .fetch_one(&self.store.db) 235 | .await 236 | .into_protocol_error()?; 237 | Ok(max_id.map(|id| id + 1).unwrap_or(1)) 238 | } 239 | 240 | /// ID of the next signed pre key 241 | async fn next_signed_pre_key_id(&self) -> Result { 242 | let max_id = query_scalar!( 243 | "SELECT MAX(id) AS 'id: u32' FROM signed_pre_keys WHERE identity = ?", 244 | self.identity 245 | ) 246 | .fetch_one(&self.store.db) 247 | .await 248 | .into_protocol_error()?; 249 | Ok(max_id.map(|id| id + 1).unwrap_or(1)) 250 | } 251 | 252 | /// ID of the next PQ pre key 253 | async fn next_pq_pre_key_id(&self) -> Result { 254 | let max_id = query_scalar!( 255 | "SELECT MAX(id) AS 'id: u32' FROM kyber_pre_keys WHERE identity = ?", 256 | self.identity 257 | ) 258 | .fetch_one(&self.store.db) 259 | .await 260 | .into_protocol_error()?; 261 | Ok(max_id.map(|id| id + 1).unwrap_or(1)) 262 | } 263 | 264 | /// number of signed pre-keys we currently have in store 265 | async fn signed_pre_keys_count(&self) -> Result { 266 | query_scalar!( 267 | "SELECT COUNT(id) FROM signed_pre_keys WHERE identity = ?", 268 | self.identity 269 | ) 270 | .fetch_one(&self.store.db) 271 | .await 272 | .into_protocol_error() 273 | .map(|count| count.try_into().expect("invalid usize")) 274 | } 275 | 276 | /// number of kyber pre-keys we currently have in store 277 | async fn kyber_pre_keys_count(&self, _last_resort: bool) -> Result { 278 | query_scalar!( 279 | "SELECT COUNT(id) FROM kyber_pre_keys WHERE identity = ?", 280 | self.identity 281 | ) 282 | .fetch_one(&self.store.db) 283 | .await 284 | .into_protocol_error() 285 | .map(|count| count.try_into().expect("invalid usize")) 286 | } 287 | } 288 | 289 | #[async_trait(?Send)] 290 | impl SignedPreKeyStore for SqliteProtocolStore { 291 | /// Look up the signed pre-key corresponding to `signed_prekey_id`. 292 | async fn get_signed_pre_key( 293 | &self, 294 | signed_prekey_id: SignedPreKeyId, 295 | ) -> Result { 296 | let id: u32 = signed_prekey_id.into(); 297 | let bytes = query_scalar!( 298 | "SELECT record FROM signed_pre_keys WHERE id = ? AND identity = ?", 299 | id, 300 | self.identity, 301 | ) 302 | .fetch_one(&self.store.db) 303 | .await 304 | .map_err(SqliteStoreError::from)?; 305 | SignedPreKeyRecord::deserialize(&bytes) 306 | } 307 | 308 | /// Set the entry for `signed_prekey_id` to the value of `record`. 309 | async fn save_signed_pre_key( 310 | &mut self, 311 | signed_prekey_id: SignedPreKeyId, 312 | record: &SignedPreKeyRecord, 313 | ) -> Result<(), SignalProtocolError> { 314 | let id: u32 = signed_prekey_id.into(); 315 | let bytes = record.serialize()?; 316 | query!( 317 | "INSERT INTO signed_pre_keys (id, identity, record) 318 | VALUES (?1, ?2, ?3) 319 | ON CONFLICT DO UPDATE SET record = ?3", 320 | id, 321 | self.identity, 322 | bytes, 323 | ) 324 | .execute(&self.store.db) 325 | .await 326 | .into_protocol_error()?; 327 | Ok(()) 328 | } 329 | } 330 | 331 | #[async_trait(?Send)] 332 | impl KyberPreKeyStore for SqliteProtocolStore { 333 | /// Look up the signed kyber pre-key corresponding to `kyber_prekey_id`. 334 | async fn get_kyber_pre_key( 335 | &self, 336 | kyber_prekey_id: KyberPreKeyId, 337 | ) -> Result { 338 | let id: u32 = kyber_prekey_id.into(); 339 | let bytes = query_scalar!( 340 | "SELECT record FROM kyber_pre_keys WHERE id = ? AND identity = ?", 341 | id, 342 | self.identity, 343 | ) 344 | .fetch_one(&self.store.db) 345 | .await 346 | .into_protocol_error()?; 347 | KyberPreKeyRecord::deserialize(&bytes) 348 | } 349 | 350 | /// Set the entry for `kyber_prekey_id` to the value of `record`. 351 | async fn save_kyber_pre_key( 352 | &mut self, 353 | kyber_prekey_id: KyberPreKeyId, 354 | record: &KyberPreKeyRecord, 355 | ) -> Result<(), SignalProtocolError> { 356 | let id: u32 = kyber_prekey_id.into(); 357 | let record = record.serialize()?; 358 | query!( 359 | "INSERT INTO kyber_pre_keys (id, identity, record) 360 | VALUES (?1, ?2, ?3) 361 | ON CONFLICT DO UPDATE SET record = ?3", 362 | id, 363 | self.identity, 364 | record, 365 | ) 366 | .execute(&self.store.db) 367 | .await 368 | .into_protocol_error()?; 369 | Ok(()) 370 | } 371 | 372 | /// Mark the entry for `kyber_prekey_id` as "used". 373 | /// This would mean different things for one-time and last-resort Kyber keys. 374 | async fn mark_kyber_pre_key_used( 375 | &mut self, 376 | kyber_prekey_id: KyberPreKeyId, 377 | ) -> Result<(), SignalProtocolError> { 378 | let id: u32 = kyber_prekey_id.into(); 379 | query!( 380 | "DELETE FROM kyber_pre_keys WHERE id = ? AND identity = ?", 381 | id, 382 | self.identity, 383 | ) 384 | .execute(&self.store.db) 385 | .await 386 | .into_protocol_error()?; 387 | Ok(()) 388 | } 389 | } 390 | 391 | #[async_trait(?Send)] 392 | impl KyberPreKeyStoreExt for SqliteProtocolStore { 393 | async fn store_last_resort_kyber_pre_key( 394 | &mut self, 395 | kyber_prekey_id: KyberPreKeyId, 396 | record: &KyberPreKeyRecord, 397 | ) -> Result<(), SignalProtocolError> { 398 | let id: u32 = kyber_prekey_id.into(); 399 | let record = record.serialize()?; 400 | query!( 401 | "INSERT INTO kyber_pre_keys 402 | (id, identity, is_last_resort, record) 403 | VALUES (?1, ?2, TRUE, ?3) 404 | ON CONFLICT DO UPDATE SET is_last_resort = TRUE, record = ?3", 405 | id, 406 | self.identity, 407 | record, 408 | ) 409 | .execute(&self.store.db) 410 | .await 411 | .into_protocol_error()?; 412 | Ok(()) 413 | } 414 | 415 | async fn load_last_resort_kyber_pre_keys( 416 | &self, 417 | ) -> Result, SignalProtocolError> { 418 | query_scalar!( 419 | "SELECT record FROM kyber_pre_keys 420 | WHERE identity = ? AND is_last_resort = TRUE", 421 | self.identity, 422 | ) 423 | .fetch_all(&self.store.db) 424 | .await 425 | .into_protocol_error()? 426 | .into_iter() 427 | .map(|record| KyberPreKeyRecord::deserialize(&record)) 428 | .collect() 429 | } 430 | 431 | async fn remove_kyber_pre_key( 432 | &mut self, 433 | kyber_prekey_id: KyberPreKeyId, 434 | ) -> Result<(), SignalProtocolError> { 435 | let id: u32 = kyber_prekey_id.into(); 436 | query!( 437 | "DELETE FROM kyber_pre_keys WHERE id = ? AND identity = ?", 438 | id, 439 | self.identity, 440 | ) 441 | .execute(&self.store.db) 442 | .await 443 | .into_protocol_error()?; 444 | Ok(()) 445 | } 446 | 447 | /// Analogous to markAllOneTimeKyberPreKeysStaleIfNecessary 448 | async fn mark_all_one_time_kyber_pre_keys_stale_if_necessary( 449 | &mut self, 450 | _stale_time: DateTime, 451 | ) -> Result<(), SignalProtocolError> { 452 | unimplemented!("should not be used yet") 453 | } 454 | 455 | /// Analogue of deleteAllStaleOneTimeKyberPreKeys 456 | async fn delete_all_stale_one_time_kyber_pre_keys( 457 | &mut self, 458 | _threshold: DateTime, 459 | _min_count: usize, 460 | ) -> Result<(), SignalProtocolError> { 461 | unimplemented!("should not be used yet") 462 | } 463 | } 464 | 465 | #[async_trait(?Send)] 466 | impl IdentityKeyStore for SqliteProtocolStore { 467 | /// Return the single specific identity the store is assumed to represent, with private key. 468 | async fn get_identity_key_pair(&self) -> Result { 469 | let key = self.identity.identity_key_pair_key(); 470 | let bytes = query_scalar!("SELECT value FROM kv WHERE key = ?", key) 471 | .fetch_one(&self.store.db) 472 | .await 473 | .into_protocol_error()?; 474 | IdentityKeyPair::try_from(&*bytes) 475 | } 476 | 477 | /// Return a [u32] specific to this store instance. 478 | /// 479 | /// This local registration id is separate from the per-device identifier used in 480 | /// [ProtocolAddress] and should not change run over run. 481 | /// 482 | /// If the same *device* is unregistered, then registers again, the [ProtocolAddress::device_id] 483 | /// may be the same, but the store registration id returned by this method should 484 | /// be regenerated. 485 | async fn get_local_registration_id(&self) -> Result { 486 | let data = self.store.load_registration_data().await?.ok_or_else(|| { 487 | SignalProtocolError::InvalidState( 488 | "failed to load registration ID", 489 | "no registration data".into(), 490 | ) 491 | })?; 492 | Ok(data.registration_id) 493 | } 494 | 495 | /// Record an identity into the store. The identity is then considered "trusted". 496 | /// 497 | /// The return value represents whether an existing identity was replaced (`Ok(true)`). If it is 498 | /// new or hasn't changed, the return value should be `Ok(false)`. 499 | async fn save_identity( 500 | &mut self, 501 | address: &ProtocolAddress, 502 | identity: &IdentityKey, 503 | ) -> Result { 504 | let device_id: u32 = address.device_id().into(); 505 | let address = address.name(); 506 | let bytes = identity.serialize(); 507 | 508 | let mut tx = self.store.db.begin().await.into_protocol_error()?; 509 | 510 | // Note: It is faster to do the update in a separate query and only insert the record if 511 | // the update did not do anything. 512 | let is_replaced = query!( 513 | "UPDATE identities SET record = ?4 514 | WHERE address = ?1 AND device_id = ?2 AND identity = ?3", 515 | address, 516 | device_id, 517 | self.identity, 518 | bytes, 519 | ) 520 | .execute(&mut *tx) 521 | .await 522 | .into_protocol_error()? 523 | .rows_affected() 524 | != 0; 525 | 526 | if !is_replaced { 527 | query!( 528 | "INSERT INTO identities (address, device_id, identity, record) 529 | VALUES (?1, ?2, ?3, ?4)", 530 | address, 531 | device_id, 532 | self.identity, 533 | bytes, 534 | ) 535 | .execute(&mut *tx) 536 | .await 537 | .into_protocol_error()?; 538 | } 539 | 540 | tx.commit().await.into_protocol_error()?; 541 | 542 | Ok(is_replaced) 543 | } 544 | 545 | /// Return whether an identity is trusted for the role specified by `direction`. 546 | async fn is_trusted_identity( 547 | &self, 548 | address: &ProtocolAddress, 549 | identity: &IdentityKey, 550 | _direction: Direction, 551 | ) -> Result { 552 | if let Some(trusted_key) = self.get_identity(address).await? { 553 | // when we encounter some identity we know, we need to decide whether we trust it or not 554 | if identity == &trusted_key { 555 | Ok(true) 556 | } else { 557 | match self.store.trust_new_identities { 558 | OnNewIdentity::Trust => Ok(true), 559 | OnNewIdentity::Reject => Ok(false), 560 | } 561 | } 562 | } else { 563 | // when we encounter a new identity, we trust it by default 564 | warn!(%address, "trusting new identity"); 565 | Ok(true) 566 | } 567 | } 568 | 569 | /// Return the public identity for the given `address`, if known. 570 | async fn get_identity( 571 | &self, 572 | address: &ProtocolAddress, 573 | ) -> Result, SignalProtocolError> { 574 | let device_id: u32 = address.device_id().into(); 575 | let address = address.name(); 576 | query_scalar!( 577 | "SELECT record FROM identities 578 | WHERE address = ? AND device_id = ? AND identity = ?", 579 | address, 580 | device_id, 581 | self.identity, 582 | ) 583 | .fetch_optional(&self.store.db) 584 | .await 585 | .into_protocol_error()? 586 | .map(|bytes| IdentityKey::decode(&bytes)) 587 | .transpose() 588 | } 589 | } 590 | 591 | #[async_trait(?Send)] 592 | impl SenderKeyStore for SqliteProtocolStore { 593 | /// Assign `record` to the entry for `(sender, distribution_id)`. 594 | async fn store_sender_key( 595 | &mut self, 596 | sender: &ProtocolAddress, 597 | distribution_id: Uuid, 598 | record: &SenderKeyRecord, 599 | ) -> Result<(), SignalProtocolError> { 600 | let address = sender.name(); 601 | let device_id: u32 = sender.device_id().into(); 602 | let record = record.serialize()?; 603 | query!( 604 | "INSERT INTO sender_keys 605 | (address, device_id, identity, distribution_id, record) 606 | VALUES (?1, ?2, ?3, ?4, ?5) 607 | ON CONFLICT DO UPDATE SET record = ?5", 608 | address, 609 | device_id, 610 | self.identity, 611 | distribution_id, 612 | record, 613 | ) 614 | .execute(&self.store.db) 615 | .await 616 | .into_protocol_error()?; 617 | Ok(()) 618 | } 619 | 620 | /// Look up the entry corresponding to `(sender, distribution_id)`. 621 | async fn load_sender_key( 622 | &mut self, 623 | sender: &ProtocolAddress, 624 | distribution_id: Uuid, 625 | ) -> Result, SignalProtocolError> { 626 | let address = sender.name(); 627 | let device_id: u32 = sender.device_id().into(); 628 | query_scalar!( 629 | "SELECT record FROM sender_keys 630 | WHERE address = ? AND device_id = ? AND identity = ? AND distribution_id = ?", 631 | address, 632 | device_id, 633 | self.identity, 634 | distribution_id, 635 | ) 636 | .fetch_optional(&self.store.db) 637 | .await 638 | .into_protocol_error()? 639 | .map(|record| SenderKeyRecord::deserialize(&record)) 640 | .transpose() 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /presage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | # be a sign or warning of (an imminent event, typically an unwelcome one). 3 | name = "presage" 4 | version = "0.7.0-dev" 5 | authors = ["Gabriel Féron "] 6 | edition = "2021" 7 | license = "AGPL-3.0-only" 8 | 9 | [dependencies] 10 | libsignal-service = { git = "https://github.com/whisperfish/libsignal-service-rs", rev = "f94d55bf8d742699024c26ca3965e85fc9946e23" } 11 | 12 | base64 = "0.22" 13 | futures = "0.3" 14 | hex = "0.4.3" 15 | rand = "0.8" 16 | serde = "1.0" 17 | serde_json = "1.0" 18 | sha2 = "0.10.8" 19 | thiserror = "1.0" 20 | tokio = { version = "1.43", default-features = false, features = [ 21 | "rt", 22 | "sync", 23 | "time", 24 | ] } 25 | tracing = "0.1" 26 | url = "2.5" 27 | serde_with = "3.11.0" 28 | derivative = "2.2.0" 29 | bytes = { version = "1.7.2", features = ["serde"] } 30 | 31 | [dev-dependencies] 32 | quickcheck = "1.0.3" 33 | quickcheck_async = "0.1" 34 | presage-store-sled = { path = "../presage-store-sled" } 35 | -------------------------------------------------------------------------------- /presage/src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use libsignal_service::prelude::MessageSenderError; 4 | use libsignal_service::{models::ParseContactError, protocol::SignalProtocolError}; 5 | 6 | use crate::store::StoreError; 7 | 8 | /// The error type of Signal manager 9 | #[derive(thiserror::Error, Debug)] 10 | #[non_exhaustive] 11 | pub enum Error { 12 | #[error("captcha from https://signalcaptchas.org/registration/generate.html required")] 13 | CaptchaRequired, 14 | #[error("input/output error: {0}")] 15 | IoError(#[from] std::io::Error), 16 | #[error("JSON error: {0}")] 17 | JsonError(#[from] serde_json::Error), 18 | #[error("error decoding base64 data: {0}")] 19 | Base64Error(#[from] base64::DecodeError), 20 | #[error("wrong slice size: {0}")] 21 | TryFromSliceError(#[from] std::array::TryFromSliceError), 22 | #[error("phone number parsing error: {0}")] 23 | PhoneNumberError(#[from] libsignal_service::prelude::phonenumber::ParseError), 24 | #[error("UUID decoding error: {0}")] 25 | UuidError(#[from] libsignal_service::prelude::UuidError), 26 | #[error("libsignal-protocol error: {0}")] 27 | ProtocolError(#[from] SignalProtocolError), 28 | #[error("libsignal-service error: {0}")] 29 | ServiceError(#[from] libsignal_service::prelude::ServiceError), 30 | #[error("libsignal-service error: {0}")] 31 | ProfileManagerError(#[from] libsignal_service::ProfileManagerError), 32 | #[error("libsignal-service sending error: {0}")] 33 | MessageSenderError(Box), 34 | #[error("this client is already registered with Signal")] 35 | AlreadyRegisteredError, 36 | #[error("this client is not yet registered, please register or link as a secondary device")] 37 | NotYetRegisteredError, 38 | #[error("failed to provision device: {0}")] 39 | ProvisioningError(#[from] libsignal_service::provisioning::ProvisioningError), 40 | #[error("no provisioning message received")] 41 | NoProvisioningMessageReceived, 42 | #[error("qr code error")] 43 | LinkingError, 44 | #[error("missing key {0} in config DB")] 45 | MissingKeyError(Cow<'static, str>), 46 | #[error("message pipe not started, you need to start receiving messages before you can send anything back")] 47 | MessagePipeNotStarted, 48 | #[error("receiving pipe was interrupted")] 49 | MessagePipeInterruptedError, 50 | #[error("failed to parse contact information: {0}")] 51 | ParseContactError(#[from] ParseContactError), 52 | #[error("failed to decrypt attachment: {0}")] 53 | AttachmentCipherError(#[from] libsignal_service::attachment_cipher::AttachmentCipherError), 54 | #[error("unknown group")] 55 | UnknownGroup, 56 | #[error("unknown recipient")] 57 | UnknownRecipient, 58 | #[error("timeout: {0}")] 59 | Timeout(#[from] tokio::time::error::Elapsed), 60 | #[error("store error: {0}")] 61 | Store(S), 62 | #[error("push challenge required (not implemented)")] 63 | PushChallengeRequired, 64 | #[error("Not allowed to request verification code, reason unknown: {0:?}")] 65 | RequestingCodeForbidden(libsignal_service::push_service::RegistrationSessionMetadataResponse), 66 | #[error("attachment sha256 checksum did not match")] 67 | UnexpectedAttachmentChecksum, 68 | #[error("Unverified registration session (i.e. wrong verification code)")] 69 | UnverifiedRegistrationSession, 70 | #[error("profile cipher error")] 71 | ProfileCipherError(#[from] libsignal_service::profile_cipher::ProfileCipherError), 72 | #[error("An operation was requested that requires the registration to be primary, but it was only secondary")] 73 | NotPrimaryDevice, 74 | } 75 | 76 | impl From for Error { 77 | fn from(v: MessageSenderError) -> Self { 78 | Self::MessageSenderError(Box::new(v)) 79 | } 80 | } 81 | 82 | impl From for Error { 83 | fn from(e: S) -> Self { 84 | Self::Store(e) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /presage/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | pub mod manager; 3 | pub mod model; 4 | mod serde; 5 | pub mod store; 6 | 7 | pub use libsignal_service; 8 | /// Protobufs used in Signal protocol and service communication 9 | pub use libsignal_service::proto; 10 | 11 | pub use errors::Error; 12 | pub use manager::Manager; 13 | 14 | const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "-rs-", env!("CARGO_PKG_VERSION")); 15 | 16 | pub type AvatarBytes = Vec; 17 | -------------------------------------------------------------------------------- /presage/src/manager/confirmation.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use libsignal_service::configuration::{ServiceConfiguration, SignalServers}; 4 | use libsignal_service::messagepipe::ServiceCredentials; 5 | use libsignal_service::prelude::phonenumber::PhoneNumber; 6 | use libsignal_service::prelude::PushService; 7 | use libsignal_service::protocol::IdentityKeyPair; 8 | use libsignal_service::provisioning::generate_registration_id; 9 | use libsignal_service::push_service::{ 10 | AccountAttributes, DeviceCapabilities, RegistrationMethod, ServiceIds, VerifyAccountResponse, 11 | }; 12 | use libsignal_service::zkgroup::profiles::ProfileKey; 13 | use libsignal_service::AccountManager; 14 | use rand::{thread_rng, RngCore}; 15 | use tracing::trace; 16 | 17 | use crate::manager::registered::RegistrationData; 18 | use crate::store::Store; 19 | use crate::{Error, Manager}; 20 | 21 | use super::Registered; 22 | 23 | /// Manager state after a successful registration of new main device 24 | /// 25 | /// In this state, the user has to confirm the new registration via a validation code. 26 | #[derive(Clone)] 27 | pub struct Confirmation { 28 | pub(crate) signal_servers: SignalServers, 29 | pub(crate) phone_number: PhoneNumber, 30 | pub(crate) password: String, 31 | pub(crate) session_id: String, 32 | } 33 | 34 | impl Manager { 35 | /// Confirm a newly registered account using the code you 36 | /// received by SMS or phone call. 37 | /// 38 | /// Returns a [registered manager](Manager::load_registered) that you can use 39 | /// to send and receive messages. 40 | pub async fn confirm_verification_code( 41 | self, 42 | confirmation_code: impl AsRef, 43 | ) -> Result, Error> { 44 | trace!("confirming verification code"); 45 | 46 | let mut rng = thread_rng(); 47 | 48 | let registration_id = generate_registration_id(&mut rng); 49 | let pni_registration_id = generate_registration_id(&mut rng); 50 | 51 | let Confirmation { 52 | signal_servers, 53 | phone_number, 54 | password, 55 | session_id, 56 | } = &*self.state; 57 | 58 | let credentials = ServiceCredentials { 59 | aci: None, 60 | pni: None, 61 | phonenumber: self.state.phone_number.clone(), 62 | password: Some(self.state.password.clone()), 63 | signaling_key: None, 64 | device_id: None, 65 | }; 66 | 67 | let service_configuration: ServiceConfiguration = signal_servers.into(); 68 | let mut identified_push_service = 69 | PushService::new(service_configuration, Some(credentials), crate::USER_AGENT); 70 | 71 | let session = identified_push_service 72 | .submit_verification_code(session_id, confirmation_code.as_ref()) 73 | .await?; 74 | 75 | trace!("verification code submitted"); 76 | 77 | if !session.verified { 78 | return Err(Error::UnverifiedRegistrationSession); 79 | } 80 | 81 | // generate a 52 bytes signaling key 82 | let mut signaling_key = [0u8; 52]; 83 | rng.fill_bytes(&mut signaling_key); 84 | 85 | // generate a 32 bytes profile key 86 | let mut profile_key = [0u8; 32]; 87 | rng.fill_bytes(&mut profile_key); 88 | let profile_key = ProfileKey::generate(profile_key); 89 | 90 | // generate new identity keys used in `register_account` and below 91 | self.store 92 | .set_aci_identity_key_pair(IdentityKeyPair::generate(&mut rng)) 93 | .await?; 94 | self.store 95 | .set_pni_identity_key_pair(IdentityKeyPair::generate(&mut rng)) 96 | .await?; 97 | 98 | let skip_device_transfer = true; 99 | let mut account_manager = AccountManager::new(identified_push_service, Some(profile_key)); 100 | 101 | let VerifyAccountResponse { 102 | aci, 103 | pni, 104 | storage_capable: _, 105 | number: _, 106 | } = account_manager 107 | .register_account( 108 | &mut rng, 109 | RegistrationMethod::SessionId(&session.id), 110 | AccountAttributes { 111 | signaling_key: Some(signaling_key.to_vec()), 112 | registration_id, 113 | pni_registration_id, 114 | voice: false, 115 | video: false, 116 | fetches_messages: true, 117 | pin: None, 118 | registration_lock: None, 119 | unidentified_access_key: Some(profile_key.derive_access_key().to_vec()), 120 | unrestricted_unidentified_access: false, // TODO: make this configurable? 121 | discoverable_by_phone_number: true, 122 | name: None, 123 | capabilities: DeviceCapabilities::default(), 124 | }, 125 | &mut self.store.aci_protocol_store(), 126 | &mut self.store.pni_protocol_store(), 127 | skip_device_transfer, 128 | ) 129 | .await?; 130 | 131 | trace!("confirmed! (and registered)"); 132 | 133 | let mut manager = Manager { 134 | store: self.store, 135 | state: Arc::new(Registered::with_data(RegistrationData { 136 | signal_servers: self.state.signal_servers, 137 | device_name: None, 138 | phone_number: phone_number.clone(), 139 | service_ids: ServiceIds { aci, pni }, 140 | password: password.clone(), 141 | signaling_key, 142 | device_id: None, 143 | registration_id, 144 | pni_registration_id: Some(pni_registration_id), 145 | profile_key, 146 | })), 147 | }; 148 | 149 | manager 150 | .store 151 | .save_registration_data(&manager.state.data) 152 | .await?; 153 | 154 | if let Err(e) = manager.register_pre_keys().await { 155 | // clear the entire store on any error, there's no possible recovery here 156 | manager.store.clear_registration().await?; 157 | Err(e) 158 | } else { 159 | Ok(manager) 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /presage/src/manager/linking.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::channel::{mpsc, oneshot}; 4 | use futures::{future, StreamExt}; 5 | use libsignal_service::configuration::{ServiceConfiguration, SignalServers}; 6 | use libsignal_service::prelude::PushService; 7 | use libsignal_service::protocol::IdentityKeyPair; 8 | use libsignal_service::provisioning::{ 9 | link_device, NewDeviceRegistration, SecondaryDeviceProvisioning, 10 | }; 11 | use rand::distributions::{Alphanumeric, DistString}; 12 | use rand::{thread_rng, RngCore}; 13 | use tracing::info; 14 | use url::Url; 15 | 16 | use crate::manager::registered::RegistrationData; 17 | use crate::store::Store; 18 | use crate::{Error, Manager}; 19 | 20 | use super::Registered; 21 | 22 | /// Manager state where it is possible to link a new secondary device 23 | pub struct Linking; 24 | 25 | impl Manager { 26 | /// Links this client as a secondary device from the device used to register the account (usually a phone). 27 | /// The URL to present to the user will be sent in the channel given as the argument. 28 | /// 29 | /// ```no_run 30 | /// use futures::{channel::oneshot, future, StreamExt}; 31 | /// use presage::libsignal_service::configuration::SignalServers; 32 | /// use presage::Manager; 33 | /// use presage::model::identity::OnNewIdentity; 34 | /// use presage_store_sled::{MigrationConflictStrategy, SledStore}; 35 | /// 36 | /// #[tokio::main] 37 | /// async fn main() -> Result<(), Box> { 38 | /// let store = 39 | /// SledStore::open("/tmp/presage-example", MigrationConflictStrategy::Drop, OnNewIdentity::Trust).await?; 40 | /// 41 | /// let (mut tx, mut rx) = oneshot::channel(); 42 | /// let (manager, err) = future::join( 43 | /// Manager::link_secondary_device( 44 | /// store, 45 | /// SignalServers::Production, 46 | /// "my-linked-client".into(), 47 | /// tx, 48 | /// ), 49 | /// async move { 50 | /// match rx.await { 51 | /// Ok(url) => println!("Show URL {} as QR code to user", url), 52 | /// Err(e) => println!("Error linking device: {}", e), 53 | /// } 54 | /// }, 55 | /// ) 56 | /// .await; 57 | /// 58 | /// Ok(()) 59 | /// } 60 | /// ``` 61 | pub async fn link_secondary_device( 62 | mut store: S, 63 | signal_servers: SignalServers, 64 | device_name: String, 65 | provisioning_link_channel: oneshot::Sender, 66 | ) -> Result, Error> { 67 | // clear the database: the moment we start the process, old API credentials are invalidated 68 | // and you won't be able to use this client anyways 69 | store.clear_registration().await?; 70 | 71 | // generate a random alphanumeric 24 chars password 72 | let mut rng = thread_rng(); 73 | let password = Alphanumeric.sample_string(&mut rng, 24); 74 | 75 | // generate a 52 bytes signaling key 76 | let mut signaling_key = [0u8; 52]; 77 | rng.fill_bytes(&mut signaling_key); 78 | 79 | let service_configuration: ServiceConfiguration = signal_servers.into(); 80 | let push_service = PushService::new(service_configuration, None, crate::USER_AGENT); 81 | 82 | let (tx, mut rx) = mpsc::channel(1); 83 | 84 | let (wait_for_qrcode_scan, registration_data) = future::join( 85 | link_device( 86 | &mut store.aci_protocol_store(), 87 | &mut store.pni_protocol_store(), 88 | &mut rng, 89 | push_service, 90 | &password, 91 | &device_name, 92 | tx, 93 | ), 94 | async move { 95 | if let Some(SecondaryDeviceProvisioning::Url(url)) = rx.next().await { 96 | info!("generating qrcode from provisioning link: {}", &url); 97 | if provisioning_link_channel.send(url).is_err() { 98 | return Err(Error::LinkingError); 99 | } 100 | } else { 101 | return Err(Error::LinkingError); 102 | } 103 | if let Some(SecondaryDeviceProvisioning::NewDeviceRegistration(data)) = 104 | rx.next().await 105 | { 106 | Ok(data) 107 | } else { 108 | Err(Error::NoProvisioningMessageReceived) 109 | } 110 | }, 111 | ) 112 | .await; 113 | 114 | wait_for_qrcode_scan?; 115 | 116 | match registration_data { 117 | Ok(NewDeviceRegistration { 118 | phone_number, 119 | device_id, 120 | registration_id, 121 | pni_registration_id, 122 | service_ids, 123 | aci_private_key, 124 | aci_public_key, 125 | pni_private_key, 126 | pni_public_key, 127 | profile_key, 128 | }) => { 129 | let registration_data = RegistrationData { 130 | signal_servers, 131 | device_name: Some(device_name), 132 | phone_number, 133 | service_ids, 134 | password, 135 | signaling_key, 136 | device_id: Some(device_id.into()), 137 | registration_id, 138 | pni_registration_id: Some(pni_registration_id), 139 | profile_key, 140 | }; 141 | 142 | store 143 | .set_aci_identity_key_pair(IdentityKeyPair::new( 144 | aci_public_key, 145 | aci_private_key, 146 | )) 147 | .await?; 148 | store 149 | .set_pni_identity_key_pair(IdentityKeyPair::new( 150 | pni_public_key, 151 | pni_private_key, 152 | )) 153 | .await?; 154 | 155 | store.save_registration_data(®istration_data).await?; 156 | info!( 157 | "successfully registered device {}", 158 | ®istration_data.service_ids 159 | ); 160 | 161 | let mut manager = Manager { 162 | store: store.clone(), 163 | state: Arc::new(Registered::with_data(registration_data)), 164 | }; 165 | 166 | // Register pre-keys with the server. If this fails, this can lead to issues 167 | // receiving, in that case clear the registration and propagate the error. 168 | if let Err(e) = manager.register_pre_keys().await { 169 | store.clear_registration().await?; 170 | Err(e) 171 | } else { 172 | Ok(manager) 173 | } 174 | } 175 | Err(e) => { 176 | store.clear_registration().await?; 177 | Err(e) 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /presage/src/manager/mod.rs: -------------------------------------------------------------------------------- 1 | //! Signal manager and its states 2 | 3 | mod confirmation; 4 | mod linking; 5 | mod registered; 6 | mod registration; 7 | 8 | use std::{fmt, sync::Arc}; 9 | 10 | pub use self::confirmation::Confirmation; 11 | pub use self::linking::Linking; 12 | pub use self::registered::{Registered, RegistrationData, RegistrationType}; 13 | pub use self::registration::{Registration, RegistrationOptions}; 14 | 15 | /// Signal manager 16 | /// 17 | /// The manager is parametrized over the [`crate::store::Store`] which stores the configuration, keys and 18 | /// optionally messages; and over the type state which describes what is the current state of the 19 | /// manager: in registration, linking, TODO 20 | /// 21 | /// Depending on the state specific methods are available or not. 22 | pub struct Manager { 23 | /// Implementation of a metadata and messages store 24 | store: Store, 25 | /// Part of the manager which is persisted in the store. 26 | state: Arc, 27 | } 28 | 29 | impl Clone for Manager { 30 | fn clone(&self) -> Self { 31 | Self { 32 | store: self.store.clone(), 33 | state: self.state.clone(), 34 | } 35 | } 36 | } 37 | 38 | impl fmt::Debug for Manager { 39 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | f.debug_struct("Manager") 41 | .field("state", &self.state) 42 | .finish_non_exhaustive() 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use super::*; 49 | 50 | #[test] 51 | fn managers_are_sync() { 52 | fn is_sync() {} 53 | 54 | // Store trait has Send + Sync as super-trait, test States only 55 | is_sync::>(); 56 | is_sync::>(); 57 | is_sync::>(); 58 | is_sync::>(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /presage/src/manager/registration.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use libsignal_service::configuration::{ServiceConfiguration, SignalServers}; 4 | use libsignal_service::prelude::phonenumber::PhoneNumber; 5 | use libsignal_service::push_service::{PushService, VerificationTransport}; 6 | use rand::distributions::{Alphanumeric, DistString}; 7 | use rand::thread_rng; 8 | use tracing::trace; 9 | 10 | use crate::store::Store; 11 | use crate::{Error, Manager}; 12 | 13 | use super::Confirmation; 14 | 15 | /// Options when registering a new main device 16 | #[derive(Debug)] 17 | pub struct RegistrationOptions<'a> { 18 | pub signal_servers: SignalServers, 19 | pub phone_number: PhoneNumber, 20 | pub use_voice_call: bool, 21 | pub captcha: Option<&'a str>, 22 | pub force: bool, 23 | } 24 | 25 | /// Manager state where it is possible to register a new main device 26 | pub struct Registration; 27 | 28 | impl Manager { 29 | /// Registers a new account with a phone number (and some options). 30 | /// 31 | /// The returned value is a [confirmation manager](Manager::confirm_verification_code) which you then 32 | /// have to use to send the confirmation code. 33 | /// 34 | /// ```no_run 35 | /// use std::str::FromStr; 36 | /// 37 | /// use presage::libsignal_service::{ 38 | /// configuration::SignalServers, prelude::phonenumber::PhoneNumber, 39 | /// }; 40 | /// use presage::manager::RegistrationOptions; 41 | /// use presage::Manager; 42 | /// use presage::model::identity::OnNewIdentity; 43 | /// 44 | /// use presage_store_sled::{MigrationConflictStrategy, SledStore}; 45 | /// 46 | /// #[tokio::main] 47 | /// async fn main() -> Result<(), Box> { 48 | /// let store = 49 | /// SledStore::open("/tmp/presage-example", MigrationConflictStrategy::Drop, OnNewIdentity::Trust).await?; 50 | /// 51 | /// let manager = Manager::register( 52 | /// store, 53 | /// RegistrationOptions { 54 | /// signal_servers: SignalServers::Production, 55 | /// phone_number: PhoneNumber::from_str("+16137827274")?, 56 | /// use_voice_call: false, 57 | /// captcha: None, 58 | /// force: false, 59 | /// }, 60 | /// ) 61 | /// .await?; 62 | /// 63 | /// Ok(()) 64 | /// } 65 | /// ``` 66 | pub async fn register( 67 | mut store: S, 68 | registration_options: RegistrationOptions<'_>, 69 | ) -> Result, Error> { 70 | let RegistrationOptions { 71 | signal_servers, 72 | phone_number, 73 | use_voice_call, 74 | captcha, 75 | force, 76 | } = registration_options; 77 | 78 | // check if we are already registered 79 | if !force && store.is_registered().await { 80 | return Err(Error::AlreadyRegisteredError); 81 | } 82 | 83 | store.clear_registration().await?; 84 | 85 | // generate a random alphanumeric 24 chars password 86 | let mut rng = thread_rng(); 87 | let password = Alphanumeric.sample_string(&mut rng, 24); 88 | 89 | let service_configuration: ServiceConfiguration = signal_servers.into(); 90 | let mut push_service = PushService::new(service_configuration, None, crate::USER_AGENT); 91 | 92 | trace!("creating registration verification session"); 93 | 94 | let phone_number_string = phone_number.to_string(); 95 | let mut session = push_service 96 | .create_verification_session(&phone_number_string, None, None, None) 97 | .await?; 98 | 99 | if !session.allowed_to_request_code { 100 | if session.captcha_required() { 101 | trace!("captcha required"); 102 | if captcha.is_none() { 103 | return Err(Error::CaptchaRequired); 104 | } 105 | session = push_service 106 | .patch_verification_session(&session.id, None, None, None, captcha, None) 107 | .await? 108 | } 109 | if session.push_challenge_required() { 110 | return Err(Error::PushChallengeRequired); 111 | } 112 | } 113 | 114 | if !session.allowed_to_request_code { 115 | return Err(Error::RequestingCodeForbidden(session)); 116 | } 117 | 118 | trace!("requesting verification code"); 119 | 120 | session = push_service 121 | .request_verification_code( 122 | &session.id, 123 | crate::USER_AGENT, 124 | if use_voice_call { 125 | VerificationTransport::Voice 126 | } else { 127 | VerificationTransport::Sms 128 | }, 129 | ) 130 | .await?; 131 | 132 | let manager = Manager { 133 | store, 134 | state: Arc::new(Confirmation { 135 | signal_servers, 136 | phone_number, 137 | password, 138 | session_id: session.id, 139 | }), 140 | }; 141 | 142 | Ok(manager) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /presage/src/model/contacts.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use libsignal_service::{ 3 | models::Attachment, 4 | prelude::{phonenumber::PhoneNumber, Uuid}, 5 | proto::Verified, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | const fn default_expire_timer_version() -> u32 { 10 | 2 11 | } 12 | 13 | /// Mirror of the protobuf ContactDetails message 14 | /// but with stronger types (e.g. `ServiceAddress` instead of optional uuid and string phone numbers) 15 | /// and some helper functions 16 | #[derive(Debug, Serialize, Deserialize)] 17 | pub struct Contact { 18 | pub uuid: Uuid, 19 | pub phone_number: Option, 20 | pub name: String, 21 | pub color: Option, 22 | #[serde(skip)] 23 | pub verified: Verified, 24 | pub profile_key: Vec, 25 | pub expire_timer: u32, 26 | #[serde(default = "default_expire_timer_version")] 27 | pub expire_timer_version: u32, 28 | pub inbox_position: u32, 29 | pub archived: bool, 30 | #[serde(skip)] 31 | pub avatar: Option>, 32 | } 33 | 34 | impl From for Contact { 35 | fn from(c: libsignal_service::models::Contact) -> Self { 36 | Self { 37 | uuid: c.uuid, 38 | phone_number: c.phone_number, 39 | name: c.name, 40 | color: c.color, 41 | verified: c.verified, 42 | profile_key: c.profile_key, 43 | expire_timer: c.expire_timer, 44 | expire_timer_version: c.expire_timer_version, 45 | inbox_position: c.inbox_position, 46 | archived: c.archived, 47 | avatar: c.avatar, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /presage/src/model/groups.rs: -------------------------------------------------------------------------------- 1 | use derivative::Derivative; 2 | use libsignal_service::{ 3 | groups_v2::Role, 4 | prelude::{AccessControl, Member, ProfileKey, Timer, Uuid}, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use super::ServiceIdType; 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | pub struct Group { 12 | pub title: String, 13 | pub avatar: String, 14 | pub disappearing_messages_timer: Option, 15 | pub access_control: Option, 16 | pub revision: u32, 17 | pub members: Vec, 18 | pub pending_members: Vec, 19 | pub requesting_members: Vec, 20 | pub invite_link_password: Vec, 21 | pub description: Option, 22 | } 23 | 24 | #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] 25 | pub struct PendingMember { 26 | // for backwards compatibility 27 | pub uuid: Uuid, 28 | #[serde(default)] 29 | pub service_id_type: ServiceIdType, 30 | pub role: Role, 31 | pub added_by_uuid: Uuid, 32 | pub timestamp: u64, 33 | } 34 | 35 | #[derive(Derivative, Clone, Deserialize, Serialize)] 36 | #[derivative(Debug)] 37 | pub struct RequestingMember { 38 | pub uuid: Uuid, 39 | pub profile_key: ProfileKey, 40 | pub timestamp: u64, 41 | } 42 | 43 | impl From for Group { 44 | fn from(val: libsignal_service::groups_v2::Group) -> Self { 45 | Group { 46 | title: val.title, 47 | avatar: val.avatar, 48 | disappearing_messages_timer: val.disappearing_messages_timer, 49 | access_control: val.access_control, 50 | revision: val.revision, 51 | members: val.members, 52 | pending_members: val.pending_members.into_iter().map(Into::into).collect(), 53 | requesting_members: val.requesting_members.into_iter().map(Into::into).collect(), 54 | invite_link_password: val.invite_link_password, 55 | description: val.description, 56 | } 57 | } 58 | } 59 | 60 | impl From for PendingMember { 61 | fn from(val: libsignal_service::groups_v2::PendingMember) -> Self { 62 | PendingMember { 63 | uuid: val.address.raw_uuid(), 64 | service_id_type: val.address.kind().into(), 65 | role: val.role, 66 | added_by_uuid: val.added_by_uuid, 67 | timestamp: val.timestamp, 68 | } 69 | } 70 | } 71 | 72 | impl From for RequestingMember { 73 | fn from(val: libsignal_service::groups_v2::RequestingMember) -> Self { 74 | RequestingMember { 75 | uuid: val.uuid, 76 | profile_key: val.profile_key, 77 | timestamp: val.timestamp, 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /presage/src/model/identity.rs: -------------------------------------------------------------------------------- 1 | /// Whether to trust or reject new identities 2 | #[derive(Debug, Clone)] 3 | pub enum OnNewIdentity { 4 | Reject, 5 | Trust, 6 | } 7 | -------------------------------------------------------------------------------- /presage/src/model/messages.rs: -------------------------------------------------------------------------------- 1 | use libsignal_service::prelude::Content; 2 | 3 | #[derive(Debug)] 4 | pub enum Received { 5 | /// when the receive loop is empty, happens when opening the websocket for the first time 6 | /// once you're done synchronizing all pending messages for this registered client. 7 | QueueEmpty, 8 | 9 | /// Got contacts (only applies if linked to a primary device 10 | /// Contacts can be later queried in the store. 11 | Contacts, 12 | 13 | /// Incoming decrypted message with metadata and content 14 | Content(Box), 15 | } 16 | -------------------------------------------------------------------------------- /presage/src/model/mod.rs: -------------------------------------------------------------------------------- 1 | use libsignal_service::protocol::ServiceIdKind; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub mod contacts; 5 | pub mod groups; 6 | pub mod identity; 7 | pub mod messages; 8 | 9 | #[derive(Debug, Default, PartialEq, Eq, Deserialize, Serialize)] 10 | pub enum ServiceIdType { 11 | /// Account Identity (ACI) 12 | /// 13 | /// An account UUID without an associated phone number, probably in the future to a username 14 | #[default] 15 | AccountIdentity, 16 | /// Phone number identity (PNI) 17 | /// 18 | /// A UUID associated with a phone number 19 | PhoneNumberIdentity, 20 | } 21 | 22 | impl From for ServiceIdType { 23 | fn from(val: ServiceIdKind) -> Self { 24 | match val { 25 | ServiceIdKind::Aci => ServiceIdType::AccountIdentity, 26 | ServiceIdKind::Pni => ServiceIdType::PhoneNumberIdentity, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /presage/src/serde.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod serde_profile_key { 2 | 3 | use base64::{engine::general_purpose, Engine}; 4 | use libsignal_service::prelude::ProfileKey; 5 | use serde::{Deserialize, Deserializer, Serializer}; 6 | 7 | pub(crate) fn serialize(profile_key: &ProfileKey, serializer: S) -> Result 8 | where 9 | S: Serializer, 10 | { 11 | serializer.serialize_str(&general_purpose::STANDARD.encode(profile_key.bytes)) 12 | } 13 | 14 | pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result 15 | where 16 | D: Deserializer<'de>, 17 | { 18 | let bytes: [u8; 32] = general_purpose::STANDARD 19 | .decode(String::deserialize(deserializer)?) 20 | .map_err(serde::de::Error::custom)? 21 | .try_into() 22 | .map_err(|e: Vec| serde::de::Error::invalid_length(e.len(), &"32 bytes"))?; 23 | Ok(ProfileKey::create(bytes)) 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::*; 29 | 30 | #[test] 31 | fn test_serialize_deserialize() { 32 | let profile_key = ProfileKey { 33 | bytes: *b"kaijpqxdvaiaeaulmsrozckjkgbpjowc", 34 | }; 35 | let mut serializer = serde_json::Serializer::new(Vec::new()); 36 | serialize(&profile_key, &mut serializer).unwrap(); 37 | let json = String::from_utf8(serializer.into_inner()).unwrap(); 38 | assert_eq!(json, "\"a2FpanBxeGR2YWlhZWF1bG1zcm96Y2tqa2dicGpvd2M=\""); 39 | 40 | let mut deserializer = serde_json::Deserializer::from_slice(json.as_bytes()); 41 | let profile_key2: ProfileKey = deserialize(&mut deserializer).unwrap(); 42 | assert_eq!(profile_key.bytes, profile_key2.bytes); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /presage/src/store.rs: -------------------------------------------------------------------------------- 1 | //! Traits that are used by the manager for storing the data. 2 | 3 | use std::{fmt, future::Future, ops::RangeBounds, time::SystemTime}; 4 | 5 | use libsignal_service::{ 6 | content::{ContentBody, Metadata}, 7 | groups_v2::Timer, 8 | pre_keys::PreKeysStore, 9 | prelude::{Content, ProfileKey, Uuid, UuidError}, 10 | proto::{ 11 | sync_message::{self, Sent}, 12 | verified, DataMessage, EditMessage, GroupContextV2, SyncMessage, Verified, 13 | }, 14 | protocol::{ 15 | IdentityKey, IdentityKeyPair, ProtocolAddress, ProtocolStore, SenderCertificate, 16 | SenderKeyStore, ServiceId, 17 | }, 18 | session_store::SessionStoreExt, 19 | zkgroup::GroupMasterKeyBytes, 20 | Profile, 21 | }; 22 | use serde::{Deserialize, Serialize}; 23 | use tracing::trace; 24 | 25 | use crate::{ 26 | manager::RegistrationData, 27 | model::{contacts::Contact, groups::Group}, 28 | AvatarBytes, 29 | }; 30 | 31 | /// An error trait implemented by store error types 32 | pub trait StoreError: std::error::Error + Sync + Send {} 33 | 34 | /// Stores the registered state of the manager 35 | pub trait StateStore { 36 | type StateStoreError: StoreError; 37 | 38 | /// Load registered (or linked) state 39 | fn load_registration_data( 40 | &self, 41 | ) -> impl Future, Self::StateStoreError>>; 42 | 43 | fn set_aci_identity_key_pair( 44 | &self, 45 | key_pair: IdentityKeyPair, 46 | ) -> impl Future>; 47 | 48 | fn set_pni_identity_key_pair( 49 | &self, 50 | key_pair: IdentityKeyPair, 51 | ) -> impl Future>; 52 | 53 | /// Save registered (or linked) state 54 | fn save_registration_data( 55 | &mut self, 56 | state: &RegistrationData, 57 | ) -> impl Future>; 58 | 59 | fn sender_certificate( 60 | &self, 61 | ) -> impl Future, Self::StateStoreError>>; 62 | 63 | fn save_sender_certificate( 64 | &self, 65 | certificate: &SenderCertificate, 66 | ) -> impl Future>; 67 | 68 | /// Returns whether this store contains registration data or not 69 | fn is_registered(&self) -> impl Future; 70 | 71 | /// Clear registration data (including keys), but keep received messages, groups and contacts. 72 | fn clear_registration(&mut self) -> impl Future>; 73 | } 74 | 75 | /// Stores messages, contacts, groups and profiles 76 | pub trait ContentsStore: Send + Sync { 77 | type ContentsStoreError: StoreError; 78 | 79 | /// Iterator over the contacts 80 | type ContactsIter: Iterator>; 81 | 82 | /// Iterator over all stored groups 83 | /// 84 | /// Each items is a tuple consisting of the group master key and its corresponding data. 85 | type GroupsIter: Iterator>; 86 | 87 | /// Iterator over all stored messages 88 | type MessagesIter: Iterator>; 89 | 90 | /// Iterator over all stored sticker packs 91 | type StickerPacksIter: Iterator>; 92 | 93 | // Clear all profiles 94 | fn clear_profiles(&mut self) -> impl Future>; 95 | 96 | // Clear all stored messages 97 | fn clear_contents(&mut self) -> impl Future>; 98 | 99 | // Messages 100 | 101 | /// Clear all stored messages. 102 | fn clear_messages(&mut self) -> impl Future>; 103 | 104 | /// Clear the messages in a thread. 105 | fn clear_thread( 106 | &mut self, 107 | thread: &Thread, 108 | ) -> impl Future>; 109 | 110 | /// Save a message in a [Thread] identified by a timestamp. 111 | fn save_message( 112 | &self, 113 | thread: &Thread, 114 | message: Content, 115 | ) -> impl Future>; 116 | 117 | /// Delete a single message, identified by its received timestamp from a thread. 118 | /// Useful when you want to delete a message locally only. 119 | fn delete_message( 120 | &mut self, 121 | thread: &Thread, 122 | timestamp: u64, 123 | ) -> impl Future>; 124 | 125 | /// Retrieve a message from a [Thread] by its timestamp. 126 | fn message( 127 | &self, 128 | thread: &Thread, 129 | timestamp: u64, 130 | ) -> impl Future, Self::ContentsStoreError>>; 131 | 132 | /// Retrieve all messages from a [Thread] within a range in time 133 | fn messages( 134 | &self, 135 | thread: &Thread, 136 | range: impl RangeBounds, 137 | ) -> impl Future>; 138 | 139 | /// Get the expire timer from a [Thread], which corresponds to either [Contact::expire_timer] 140 | /// or [Group::disappearing_messages_timer]. 141 | fn expire_timer( 142 | &self, 143 | thread: &Thread, 144 | ) -> impl Future, Self::ContentsStoreError>> { 145 | async move { 146 | match thread { 147 | Thread::Contact(uuid) => Ok(self 148 | .contact_by_id(uuid) 149 | .await? 150 | .map(|c| (c.expire_timer, c.expire_timer_version))), 151 | Thread::Group(key) => Ok(self 152 | .group(*key) 153 | .await? 154 | .and_then(|g| g.disappearing_messages_timer) 155 | // TODO: most likely we can have versions here 156 | .map(|t| (t.duration, 1))), // Groups do not have expire_timer_version 157 | } 158 | } 159 | } 160 | 161 | /// Update the expire timer from a [Thread], which corresponds to either [Contact::expire_timer] 162 | /// or [Group::disappearing_messages_timer]. 163 | fn update_expire_timer( 164 | &mut self, 165 | thread: &Thread, 166 | timer: u32, 167 | version: u32, 168 | ) -> impl Future> { 169 | async move { 170 | trace!(%thread, timer, version, "updating expire timer"); 171 | match thread { 172 | Thread::Contact(uuid) => { 173 | let contact = self.contact_by_id(uuid).await?; 174 | if let Some(mut contact) = contact { 175 | let current_version = contact.expire_timer_version; 176 | if version <= current_version { 177 | return Ok(()); 178 | } 179 | contact.expire_timer_version = version; 180 | contact.expire_timer = timer; 181 | self.save_contact(&contact).await?; 182 | } 183 | Ok(()) 184 | } 185 | Thread::Group(key) => { 186 | let group = self.group(*key).await?; 187 | if let Some(mut g) = group { 188 | g.disappearing_messages_timer = Some(Timer { duration: timer }); 189 | self.save_group(*key, g).await?; 190 | } 191 | Ok(()) 192 | } 193 | } 194 | } 195 | } 196 | 197 | // Contacts 198 | 199 | /// Clear all saved synchronized contact data 200 | fn clear_contacts(&mut self) -> impl Future>; 201 | 202 | /// Save a contact 203 | fn save_contact( 204 | &mut self, 205 | contacts: &Contact, 206 | ) -> impl Future>; 207 | 208 | /// Get an iterator on all stored (synchronized) contacts 209 | fn contacts( 210 | &self, 211 | ) -> impl Future>; 212 | 213 | /// Get contact data for a single user by its [Uuid]. 214 | fn contact_by_id( 215 | &self, 216 | id: &Uuid, 217 | ) -> impl Future, Self::ContentsStoreError>>; 218 | 219 | /// Delete all cached group data 220 | fn clear_groups(&mut self) -> impl Future>; 221 | 222 | /// Save a group in the cache 223 | fn save_group( 224 | &self, 225 | master_key: GroupMasterKeyBytes, 226 | group: impl Into, 227 | ) -> impl Future>; 228 | 229 | /// Get an iterator on all cached groups 230 | fn groups(&self) -> impl Future>; 231 | 232 | /// Retrieve a single unencrypted group indexed by its `[GroupMasterKeyBytes]` 233 | fn group( 234 | &self, 235 | master_key: GroupMasterKeyBytes, 236 | ) -> impl Future, Self::ContentsStoreError>>; 237 | 238 | /// Save a group avatar in the cache 239 | fn save_group_avatar( 240 | &self, 241 | master_key: GroupMasterKeyBytes, 242 | avatar: &AvatarBytes, 243 | ) -> impl Future>; 244 | 245 | /// Retrieve a group avatar from the cache. 246 | fn group_avatar( 247 | &self, 248 | master_key: GroupMasterKeyBytes, 249 | ) -> impl Future, Self::ContentsStoreError>>; 250 | 251 | // Profiles 252 | 253 | /// Insert or update the profile key of a contact 254 | fn upsert_profile_key( 255 | &mut self, 256 | uuid: &Uuid, 257 | key: ProfileKey, 258 | ) -> impl Future>; 259 | 260 | /// Get the profile key for a contact 261 | fn profile_key( 262 | &self, 263 | uuid: &Uuid, 264 | ) -> impl Future, Self::ContentsStoreError>>; 265 | 266 | /// Save a profile by [Uuid] and [ProfileKey]. 267 | fn save_profile( 268 | &mut self, 269 | uuid: Uuid, 270 | key: ProfileKey, 271 | profile: Profile, 272 | ) -> impl Future>; 273 | 274 | /// Retrieve a profile by [Uuid] and [ProfileKey]. 275 | fn profile( 276 | &self, 277 | uuid: Uuid, 278 | key: ProfileKey, 279 | ) -> impl Future, Self::ContentsStoreError>>; 280 | 281 | /// Save a profile avatar by [Uuid] and [ProfileKey]. 282 | fn save_profile_avatar( 283 | &mut self, 284 | uuid: Uuid, 285 | key: ProfileKey, 286 | profile: &AvatarBytes, 287 | ) -> impl Future>; 288 | 289 | /// Retrieve a profile avatar by [Uuid] and [ProfileKey]. 290 | fn profile_avatar( 291 | &self, 292 | uuid: Uuid, 293 | key: ProfileKey, 294 | ) -> impl Future, Self::ContentsStoreError>>; 295 | 296 | // Stickers 297 | 298 | /// Add a sticker pack 299 | fn add_sticker_pack( 300 | &mut self, 301 | pack: &StickerPack, 302 | ) -> impl Future> + Send; 303 | 304 | /// Gets a cached sticker pack 305 | fn sticker_pack( 306 | &self, 307 | id: &[u8], 308 | ) -> impl Future, Self::ContentsStoreError>>; 309 | 310 | /// Removes a sticker pack 311 | fn remove_sticker_pack( 312 | &mut self, 313 | id: &[u8], 314 | ) -> impl Future>; 315 | 316 | /// Get an iterator on all installed stickerpacks 317 | fn sticker_packs( 318 | &self, 319 | ) -> impl Future>; 320 | } 321 | 322 | /// The manager store trait combining all other stores into a single one 323 | pub trait Store: 324 | StateStore 325 | + ContentsStore 326 | + Send 327 | + Sync 328 | + Clone 329 | + 'static 330 | { 331 | type Error: StoreError; 332 | type AciStore: ProtocolStore + PreKeysStore + SenderKeyStore + SessionStoreExt + Sync + Clone; 333 | type PniStore: ProtocolStore + PreKeysStore + SenderKeyStore + SessionStoreExt + Sync + Clone; 334 | 335 | /// Clear the entire store 336 | /// 337 | /// This can be useful when resetting an existing client. 338 | fn clear( 339 | &mut self, 340 | ) -> impl Future::StateStoreError>> + Send; 341 | 342 | fn aci_protocol_store(&self) -> Self::AciStore; 343 | 344 | fn pni_protocol_store(&self) -> Self::PniStore; 345 | } 346 | 347 | /// A thread specifies where a message was sent, either to or from a contact or in a group. 348 | #[derive(Debug, Hash, Eq, PartialEq, Clone, Deserialize, Serialize)] 349 | pub enum Thread { 350 | /// The message was sent inside a contact-chat. 351 | /// TODO: make this correctly either ACI or PNI (store the ServiceId) 352 | Contact(Uuid), 353 | // Cannot use GroupMasterKey as unable to extract the bytes. 354 | /// The message was sent inside a groups-chat with the [`GroupMasterKeyBytes`] (specified as bytes). 355 | Group(GroupMasterKeyBytes), 356 | } 357 | 358 | impl fmt::Display for Thread { 359 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 360 | match self { 361 | Thread::Contact(uuid) => write!(f, "Thread(contact={uuid})"), 362 | Thread::Group(master_key_bytes) => { 363 | write!(f, "Thread(group={:x?})", &master_key_bytes[..4]) 364 | } 365 | } 366 | } 367 | } 368 | 369 | impl TryFrom<&Content> for Thread { 370 | type Error = UuidError; 371 | 372 | fn try_from(content: &Content) -> Result { 373 | match &content.body { 374 | // [1-1] Message sent by us with another device 375 | ContentBody::SynchronizeMessage(SyncMessage { 376 | sent: 377 | Some(Sent { 378 | destination_service_id: Some(uuid), 379 | .. 380 | }), 381 | .. 382 | }) => Ok(Self::Contact(Uuid::parse_str(uuid)?)), 383 | // [Group] message from somebody else 384 | ContentBody::DataMessage(DataMessage { 385 | group_v2: 386 | Some(GroupContextV2 { 387 | master_key: Some(key), 388 | .. 389 | }), 390 | .. 391 | }) 392 | // [Group] message sent by us with another device 393 | | ContentBody::SynchronizeMessage(SyncMessage { 394 | sent: 395 | Some(Sent { 396 | message: 397 | Some(DataMessage { 398 | group_v2: 399 | Some(GroupContextV2 { 400 | master_key: Some(key), 401 | .. 402 | }), 403 | .. 404 | }), 405 | .. 406 | }), 407 | .. 408 | }) 409 | // [Group] message edit sent by us with another device 410 | | ContentBody::SynchronizeMessage(SyncMessage { 411 | sent: 412 | Some(Sent { 413 | edit_message: 414 | Some(EditMessage { 415 | data_message: 416 | Some(DataMessage { 417 | group_v2: 418 | Some(GroupContextV2 { 419 | master_key: Some(key), 420 | .. 421 | }), 422 | .. 423 | }), 424 | .. 425 | }), 426 | .. 427 | }), 428 | .. 429 | }) 430 | // [Group] Message edit sent by somebody else 431 | | ContentBody::EditMessage(EditMessage { 432 | data_message: 433 | Some(DataMessage { 434 | group_v2: 435 | Some(GroupContextV2 { 436 | master_key: Some(key), 437 | .. 438 | }), 439 | .. 440 | }), 441 | .. 442 | }) => Ok(Self::Group( 443 | key.clone() 444 | .try_into() 445 | .expect("Group master key to have 32 bytes"), 446 | )), 447 | // [1-1] Any other message directly to us 448 | _ => Ok(Thread::Contact(content.metadata.sender.raw_uuid())), 449 | } 450 | } 451 | } 452 | 453 | /// Extension trait of [`Content`] 454 | pub trait ContentExt { 455 | fn timestamp(&self) -> u64; 456 | } 457 | 458 | impl ContentExt for Content { 459 | /// The original timestamp of the message. 460 | fn timestamp(&self) -> u64 { 461 | match self.body { 462 | ContentBody::SynchronizeMessage(SyncMessage { 463 | sent: 464 | Some(sync_message::Sent { 465 | timestamp: Some(ts), 466 | .. 467 | }), 468 | .. 469 | }) => ts, 470 | ContentBody::SynchronizeMessage(SyncMessage { 471 | sent: 472 | Some(sync_message::Sent { 473 | edit_message: 474 | Some(EditMessage { 475 | target_sent_timestamp: Some(ts), 476 | .. 477 | }), 478 | .. 479 | }), 480 | .. 481 | }) => ts, 482 | ContentBody::EditMessage(EditMessage { 483 | target_sent_timestamp: Some(ts), 484 | .. 485 | }) => ts, 486 | _ => self.metadata.timestamp, 487 | } 488 | } 489 | } 490 | 491 | #[derive(Debug, Clone, Serialize, Deserialize)] 492 | pub struct StickerPack { 493 | pub id: Vec, 494 | pub key: Vec, 495 | pub manifest: StickerPackManifest, 496 | } 497 | 498 | /// equivalent to [Pack](crate::proto::Pack) 499 | #[derive(Debug, Clone, Serialize, Deserialize)] 500 | pub struct StickerPackManifest { 501 | pub title: String, 502 | pub author: String, 503 | pub cover: Option, 504 | pub stickers: Vec, 505 | } 506 | 507 | impl From for StickerPackManifest { 508 | fn from(value: libsignal_service::proto::Pack) -> Self { 509 | Self { 510 | title: value.title().to_owned(), 511 | author: value.author().to_owned(), 512 | cover: value.cover.map(Into::into), 513 | stickers: value.stickers.into_iter().map(|s| s.into()).collect(), 514 | } 515 | } 516 | } 517 | 518 | /// equivalent to [Sticker](crate::proto::pack::Sticker) 519 | #[derive(Debug, Clone, Serialize, Deserialize)] 520 | pub struct Sticker { 521 | pub id: u32, 522 | pub emoji: Option, 523 | pub content_type: Option, 524 | pub bytes: Option>, 525 | } 526 | 527 | impl From for Sticker { 528 | fn from(value: libsignal_service::proto::pack::Sticker) -> Self { 529 | Self { 530 | id: value.id(), 531 | emoji: value.emoji, 532 | content_type: value.content_type, 533 | bytes: None, 534 | } 535 | } 536 | } 537 | 538 | /// Saves a message that can show users when the identity of a contact has changed 539 | /// On Signal Android, this is usually displayed as: "Your safety number with XYZ has changed." 540 | pub async fn save_trusted_identity_message( 541 | store: &S, 542 | protocol_address: &ProtocolAddress, 543 | right_identity_key: IdentityKey, 544 | verified_state: verified::State, 545 | ) -> Result<(), S::Error> { 546 | let Some(sender) = ServiceId::parse_from_service_id_string(protocol_address.name()) else { 547 | return Ok(()); 548 | }; 549 | 550 | // TODO: this is a hack to save a message showing that the verification status changed 551 | // It is possibly ok to do it like this, but rebuidling the metadata and content body feels dirty 552 | let thread = Thread::Contact(sender.raw_uuid()); 553 | let verified_sync_message = Content { 554 | metadata: Metadata { 555 | sender, 556 | destination: sender, 557 | sender_device: 0, 558 | server_guid: None, 559 | timestamp: SystemTime::now() 560 | .duration_since(SystemTime::UNIX_EPOCH) 561 | .unwrap_or_default() 562 | .as_millis() as u64, 563 | needs_receipt: false, 564 | unidentified_sender: false, 565 | was_plaintext: false, 566 | }, 567 | body: SyncMessage { 568 | verified: Some(Verified { 569 | destination_aci: None, 570 | identity_key: Some(right_identity_key.public_key().serialize().to_vec()), 571 | state: Some(verified_state.into()), 572 | null_message: None, 573 | }), 574 | ..Default::default() 575 | } 576 | .into(), 577 | }; 578 | 579 | store.save_message(&thread, verified_sync_message).await 580 | } 581 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | format_code_in_doc_comments = true 3 | --------------------------------------------------------------------------------