Skip to content

Commit d10fefd

Browse files
committed
Derive the database key from the password.
1 parent 8ef12d9 commit d10fefd

File tree

10 files changed

+263
-17
lines changed

10 files changed

+263
-17
lines changed

Cargo.lock

Lines changed: 107 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ clientele = { version = "0.3.8", default-features = false, features = [
3434
"clap",
3535
"std",
3636
], optional = true }
37+
hex = "0.4"
38+
libaes = "0.7"
39+
pbkdf2 = "0.12"
3740
rusqlite = { version = "0.37", default-features = true, features = [
3841
"bundled-sqlcipher-vendored-openssl",
3942
"jiff",
@@ -43,6 +46,7 @@ serde_json = { version = "1", default-features = false, features = [
4346
"alloc",
4447
"preserve_order",
4548
] }
49+
sha1 = "0.10"
4650

4751
[profile.release]
4852
opt-level = "z"

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,36 @@ asimov-signal-reader
3636

3737
## ⚙ Configuration
3838

39+
Signal Desktop stores data in an encrypted [SQLCipher] database. The encryption
40+
key for this database is stored in a `config.json` file in Signal's application
41+
data directory, and that key is itself encrypted using an encryption password
42+
stored in the (platform-specific) system keychain.
43+
44+
This module can be configured to decrypt the Signal database using either the
45+
encryption password or the encryption key. (You don't need both, just one.)
46+
47+
### Encryption Password
48+
49+
The simplest way to configure the module is to set the `ASIMOV_SIGNAL_PASSWORD`
50+
environment variable to the encryption password stored in the system keychain:
51+
52+
```bash
53+
# macOS
54+
export ASIMOV_SIGNAL_PASSWORD=$(security find-generic-password -a "Signal Key" -s "Signal Safe Storage" -w)
55+
```
56+
3957
### Encryption Key
4058

59+
Alternatively, for advanced users, you could set the `ASIMOV_SIGNAL_KEY`
60+
environment variable to the actual decrypted value of `encryptedKey` found in
61+
the `config.json` file:
62+
4163
```bash
4264
export ASIMOV_SIGNAL_KEY=feedc0dedecafbadcafebabecafed00dfeedc0dedecafbadcafebabecafed00d
4365
```
4466

45-
The key must be 64 hexadecimal characters, meaning 32 bytes (256 bits).
67+
This key must be 64 hexadecimal characters, meaning 32 bytes (256 bits).
68+
Deriving this key manually is well beyond the scope of these instructions.
4669

4770
## 📚 Reference
4871

@@ -90,3 +113,5 @@ git clone https://github.com/asimov-modules/asimov-signal-module.git
90113
[RDF]: https://www.w3.org/TR/rdf12-primer/
91114
[Rust]: https://rust-lang.org
92115
[Signal]: https://signal.org
116+
[Signal Desktop]: https://github.com/signalapp/Signal-Desktop
117+
[SQLCipher]: https://www.zetetic.net/sqlcipher/

src/config.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// This is free and unencumbered software released into the public domain.
22

3-
use alloc::string::String;
4-
use asimov_module::secrecy::SecretString;
3+
use super::SecretKey;
4+
use alloc::{boxed::Box, string::String};
55
use serde_json::{Map, Value};
66

77
#[derive(Debug)]
@@ -35,9 +35,21 @@ impl SignalConfig {
3535
}
3636
}
3737

38-
pub fn encrypted_key(&self) -> Option<SecretString> {
39-
self.json
40-
.get("encryptedKey")
41-
.and_then(|x| x.as_str().map(SecretString::from))
38+
pub fn key(&self) -> Option<SecretKey> {
39+
let Some(key_str) = self.json.get("key").and_then(|x| x.as_str()) else {
40+
return None;
41+
};
42+
hex::decode(key_str)
43+
.ok()
44+
.map(|key_data| SecretKey::new(Box::new(key_data)))
45+
}
46+
47+
pub fn encrypted_key(&self) -> Option<SecretKey> {
48+
let Some(key_str) = self.json.get("encryptedKey").and_then(|x| x.as_str()) else {
49+
return None;
50+
};
51+
hex::decode(key_str)
52+
.ok()
53+
.map(|key_data| SecretKey::new(Box::new(key_data)))
4254
}
4355
}

src/db.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// This is free and unencumbered software released into the public domain.
22

3+
use super::SecretKey;
34
use alloc::format;
4-
use asimov_module::secrecy::{ExposeSecret, SecretString};
5+
use asimov_module::secrecy::ExposeSecret;
56
use rusqlite::{Connection, Result};
67

78
#[derive(Debug)]
@@ -23,9 +24,10 @@ impl SignalDb {
2324
Ok(Self { conn })
2425
}
2526

26-
pub fn decrypt(&self, key: SecretString) -> Result<()> {
27+
pub fn decrypt(&self, key: SecretKey) -> Result<()> {
28+
let ascii_key = hex::encode(key.expose_secret());
2729
self.conn
28-
.pragma_update(None, "key", format!("x'{}'", key.expose_secret()))
30+
.pragma_update(None, "key", format!("x'{}'", ascii_key))
2931
}
3032

3133
pub fn is_readable(&self) -> bool {

src/decrypt.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// This is free and unencumbered software released into the public domain.
2+
3+
use super::SecretKey;
4+
use alloc::boxed::Box;
5+
use asimov_module::secrecy::ExposeSecret;
6+
use libaes::Cipher;
7+
use pbkdf2::pbkdf2_hmac;
8+
use sha1::Sha1;
9+
10+
const SALT: &[u8] = b"saltysalt";
11+
const ROUNDS: u32 = 1003;
12+
13+
pub fn decrypt_key(password: SecretKey, encrypted_key: &[u8]) -> Result<SecretKey, ()> {
14+
match encrypted_key.strip_prefix(b"v10") {
15+
Some(encrypted_key) => decrypt_key_v10(password, encrypted_key),
16+
None => Err(()),
17+
}
18+
}
19+
20+
pub fn decrypt_key_v10(password: SecretKey, encrypted_key: &[u8]) -> Result<SecretKey, ()> {
21+
// Derive the key using PBKDF2:
22+
let mut kek = [0u8; 16];
23+
pbkdf2_hmac::<Sha1>(password.expose_secret(), SALT, ROUNDS, &mut kek);
24+
25+
let cipher = Cipher::new_128(&kek);
26+
27+
let iv = [' ' as u8; 16];
28+
let decrypted_key = cipher.cbc_decrypt(&iv, encrypted_key);
29+
30+
// The decrypted key is a 64-character hex string:
31+
assert_eq!(decrypted_key.len(), 64);
32+
33+
// FIXME: decode into a 32-byte byte array:
34+
let ascii_key = std::str::from_utf8(&decrypted_key).unwrap();
35+
let decrypted_key = hex::decode(ascii_key).unwrap();
36+
37+
Ok(SecretKey::new(Box::new(decrypted_key)))
38+
}

src/dir.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// This is free and unencumbered software released into the public domain.
22

3-
use super::{SignalConfig, SignalDb};
3+
use super::{SecretKey, SignalConfig, SignalDb, decrypt_key};
4+
use asimov_module::secrecy::ExposeSecret;
45
use std::{io, path::PathBuf};
56

67
#[derive(Debug)]
@@ -9,8 +10,21 @@ pub struct SignalDir {
910
}
1011

1112
impl SignalDir {
12-
pub fn open(path: PathBuf) -> Self {
13-
Self { path }
13+
pub fn open(path: PathBuf) -> io::Result<Self> {
14+
Ok(Self { path })
15+
}
16+
17+
pub fn key(&self, password: Option<SecretKey>) -> io::Result<Option<SecretKey>> {
18+
let config = self.config()?;
19+
if let Some(key) = config.key() {
20+
return Ok(Some(key));
21+
};
22+
let Some(encrypted_key) = config.encrypted_key() else {
23+
return Ok(None);
24+
};
25+
let result = decrypt_key(password.unwrap(), encrypted_key.expose_secret())
26+
.map_err(|_err| io::Error::new(io::ErrorKind::Other, "invalid password"))?;
27+
Ok(Some(result))
1428
}
1529

1630
pub fn config(&self) -> io::Result<SignalConfig> {

0 commit comments

Comments
 (0)