check repo index timestamps to prevent rollback attacks
A hacked fdroid server could "replay" old index.jar files known to have apps with vulnerabilities in it. That provides a long window of time for exploiting that vulnerability. By checking that the timestamp of an update is never older than the current index, this attack is prevented.
This commit is contained in:
parent
014ab2d2b6
commit
02b2090e53
@ -20,6 +20,7 @@ import static org.junit.Assert.fail;
|
||||
public class RepoUpdaterTest {
|
||||
|
||||
private Context context;
|
||||
private Repo repo;
|
||||
private RepoUpdater repoUpdater;
|
||||
private File testFilesDir;
|
||||
|
||||
@ -30,10 +31,9 @@ public class RepoUpdaterTest {
|
||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
context = instrumentation.getContext();
|
||||
testFilesDir = TestUtils.getWriteableDir(instrumentation);
|
||||
Repo repo = new Repo();
|
||||
repo = new Repo();
|
||||
repo.address = "https://fake.url/fdroid/repo";
|
||||
repo.signingCertificate = this.simpleIndexSigningCert;
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -42,6 +42,7 @@ public class RepoUpdaterTest {
|
||||
return;
|
||||
}
|
||||
File simpleIndexJar = TestUtils.copyAssetToDir(context, "simpleIndex.jar", testFilesDir);
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
|
||||
// these are supposed to succeed
|
||||
try {
|
||||
@ -52,6 +53,18 @@ public class RepoUpdaterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = UpdateException.class)
|
||||
public void testExtractIndexFromOutdatedJar() throws UpdateException {
|
||||
File simpleIndexJar = TestUtils.copyAssetToDir(context, "simpleIndex.jar", testFilesDir);
|
||||
repo.version = 10;
|
||||
repo.timestamp = System.currentTimeMillis() / 1000L;
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
|
||||
// these are supposed to fail
|
||||
repoUpdater.processDownloadedFile(simpleIndexJar);
|
||||
fail();
|
||||
}
|
||||
|
||||
@Test(expected = UpdateException.class)
|
||||
public void testExtractIndexFromJarWithoutSignatureJar() throws UpdateException {
|
||||
if (!testFilesDir.canWrite()) {
|
||||
@ -59,7 +72,9 @@ public class RepoUpdaterTest {
|
||||
}
|
||||
// this is supposed to fail
|
||||
File jarFile = TestUtils.copyAssetToDir(context, "simpleIndexWithoutSignature.jar", testFilesDir);
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
repoUpdater.processDownloadedFile(jarFile);
|
||||
fail();
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -70,6 +85,7 @@ public class RepoUpdaterTest {
|
||||
// this is supposed to fail
|
||||
try {
|
||||
File jarFile = TestUtils.copyAssetToDir(context, "simpleIndexWithCorruptedManifest.jar", testFilesDir);
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
repoUpdater.processDownloadedFile(jarFile);
|
||||
fail();
|
||||
} catch (UpdateException e) {
|
||||
@ -88,6 +104,7 @@ public class RepoUpdaterTest {
|
||||
// this is supposed to fail
|
||||
try {
|
||||
File jarFile = TestUtils.copyAssetToDir(context, "simpleIndexWithCorruptedSignature.jar", testFilesDir);
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
repoUpdater.processDownloadedFile(jarFile);
|
||||
fail();
|
||||
} catch (UpdateException e) {
|
||||
@ -106,6 +123,7 @@ public class RepoUpdaterTest {
|
||||
// this is supposed to fail
|
||||
try {
|
||||
File jarFile = TestUtils.copyAssetToDir(context, "simpleIndexWithCorruptedCertificate.jar", testFilesDir);
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
repoUpdater.processDownloadedFile(jarFile);
|
||||
fail();
|
||||
} catch (UpdateException e) {
|
||||
@ -124,6 +142,7 @@ public class RepoUpdaterTest {
|
||||
// this is supposed to fail
|
||||
try {
|
||||
File jarFile = TestUtils.copyAssetToDir(context, "simpleIndexWithCorruptedEverything.jar", testFilesDir);
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
repoUpdater.processDownloadedFile(jarFile);
|
||||
fail();
|
||||
} catch (UpdateException e) {
|
||||
@ -142,6 +161,7 @@ public class RepoUpdaterTest {
|
||||
// this is supposed to fail
|
||||
try {
|
||||
File jarFile = TestUtils.copyAssetToDir(context, "masterKeyIndex.jar", testFilesDir);
|
||||
repoUpdater = new RepoUpdater(context, repo);
|
||||
repoUpdater.processDownloadedFile(jarFile);
|
||||
fail(); //NOPMD
|
||||
} catch (UpdateException e) {
|
||||
|
@ -43,6 +43,7 @@ public class RepoXMLHandlerTest {
|
||||
expectedRepo.name = "F-Droid";
|
||||
expectedRepo.signingCertificate = "308201ee30820157a0030201020204300d845b300d06092a864886f70d01010b0500302a3110300e060355040b1307462d44726f6964311630140603550403130d70616c6174736368696e6b656e301e170d3134303432373030303633315a170d3431303931323030303633315a302a3110300e060355040b1307462d44726f6964311630140603550403130d70616c6174736368696e6b656e30819f300d06092a864886f70d010101050003818d0030818902818100a439472e4b6d01141bfc94ecfe131c7c728fdda670bb14c57ca60bd1c38a8b8bc0879d22a0a2d0bc0d6fdd4cb98d1d607c2caefbe250a0bd0322aedeb365caf9b236992fac13e6675d3184a6c7c6f07f73410209e399a9da8d5d7512bbd870508eebacff8b57c3852457419434d34701ccbf692267cbc3f42f1c5d1e23762d790203010001a321301f301d0603551d0e041604140b1840691dab909746fde4bfe28207d1cae15786300d06092a864886f70d01010b05000381810062424c928ffd1b6fd419b44daafef01ca982e09341f7077fb865905087aeac882534b3bd679b51fdfb98892cef38b63131c567ed26c9d5d9163afc775ac98ad88c405d211d6187bde0b0d236381cc574ba06ef9080721a92ae5a103a7301b2c397eecc141cc850dd3e123813ebc41c59d31ddbcb6e984168280c53272f6a442b";
|
||||
expectedRepo.description = "The official repository of the F-Droid client. Applications in this repository are either official binaries built by the original application developers, or are binaries built from source by the admin of f-droid.org using the tools on https://gitorious.org/f-droid.";
|
||||
expectedRepo.timestamp = 1398733213;
|
||||
RepoDetails actualDetails = getFromFile("simpleIndex.xml");
|
||||
handlerTestSuite(expectedRepo, actualDetails, 0, 0, -1, 12);
|
||||
}
|
||||
@ -53,6 +54,7 @@ public class RepoXMLHandlerTest {
|
||||
expectedRepo.name = "Android-Nexus-7-20139453 on UNSET";
|
||||
expectedRepo.signingCertificate = "308202da308201c2a00302010202080eb08c796fec91aa300d06092a864886f70d0101050500302d3111300f060355040a0c084b6572706c61707031183016060355040b0c0f477561726469616e50726f6a656374301e170d3134313030333135303631325a170d3135313030333135303631325a302d3111300f060355040a0c084b6572706c61707031183016060355040b0c0f477561726469616e50726f6a65637430820122300d06092a864886f70d01010105000382010f003082010a0282010100c7ab44b130be5c00eedcc3625462f6f6ac26e502641cd641f3e30cbb0ff1ba325158611e7fc2448a35b6a6df30dc6e23602cf6909448befcf11e2fe486b580f1e76fe5887d159050d00afd2c4079f6538896bb200627f4b3e874f011ce5df0fef5d150fcb0b377b531254e436eaf4083ea72fe3b8c3ef450789fa858f2be8f6c5335bb326aff3dda689fbc7b5ba98dea53651dbea7452c38d294985ac5dd8a9e491a695de92c706d682d6911411fcaef3b0a08a030fe8a84e47acaab0b7edcda9d190ce39e810b79b1d8732eca22b15f0d048c8d6f00503a7ee81ab6e08919ff465883432304d95238b95e95c5f74e0a421809e2a6a85825aed680e0d6939e8f0203010001300d06092a864886f70d010105050003820101006d17aad3271b8b2c299dbdb7b1182849b0d5ddb9f1016dcb3487ae0db02b6be503344c7d066e2050bcd01d411b5ee78c7ed450f0ff9da5ce228f774cbf41240361df53d9c6078159d16f4d34379ab7dedf6186489397c83b44b964251a2ebb42b7c4689a521271b1056d3b5a5fa8f28ba64fb8ce5e2226c33c45d27ba3f632dc266c12abf582b8438c2abcf3eae9de9f31152b4158ace0ef33435c20eb809f1b3988131db6e5a1442f2617c3491d9565fedb3e320e8df4236200d3bd265e47934aa578f84d0d1a5efeb49b39907e876452c46996d0feff9404b41aa5631b4482175d843d5512ded45e12a514690646492191e7add434afce63dbff8f0b03ec0c";
|
||||
expectedRepo.description = "A local FDroid repo generated from apps installed on Android-Nexus-7-20139453";
|
||||
expectedRepo.timestamp = 1412696461;
|
||||
RepoDetails actualDetails = getFromFile("smallRepo.xml");
|
||||
handlerTestSuite(expectedRepo, actualDetails, 12, 12, 14, -1);
|
||||
checkIncludedApps(actualDetails.apps, new String[]{
|
||||
@ -77,6 +79,7 @@ public class RepoXMLHandlerTest {
|
||||
expectedRepo.name = "Guardian Project Official Releases";
|
||||
expectedRepo.signingCertificate = "308205d8308203c0020900a397b4da7ecda034300d06092a864886f70d01010505003081ad310b30090603550406130255533111300f06035504080c084e657720596f726b3111300f06035504070c084e657720596f726b31143012060355040b0c0b4644726f6964205265706f31193017060355040a0c10477561726469616e2050726f6a656374311d301b06035504030c14677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d0109011619726f6f7440677561726469616e70726f6a6563742e696e666f301e170d3134303632363139333931385a170d3431313131303139333931385a3081ad310b30090603550406130255533111300f06035504080c084e657720596f726b3111300f06035504070c084e657720596f726b31143012060355040b0c0b4644726f6964205265706f31193017060355040a0c10477561726469616e2050726f6a656374311d301b06035504030c14677561726469616e70726f6a6563742e696e666f3128302606092a864886f70d0109011619726f6f7440677561726469616e70726f6a6563742e696e666f30820222300d06092a864886f70d01010105000382020f003082020a0282020100b3cd79121b9b883843be3c4482e320809106b0a23755f1dd3c7f46f7d315d7bb2e943486d61fc7c811b9294dcc6b5baac4340f8db2b0d5e14749e7f35e1fc211fdbc1071b38b4753db201c314811bef885bd8921ad86facd6cc3b8f74d30a0b6e2e6e576f906e9581ef23d9c03e926e06d1f033f28bd1e21cfa6a0e3ff5c9d8246cf108d82b488b9fdd55d7de7ebb6a7f64b19e0d6b2ab1380a6f9d42361770d1956701a7f80e2de568acd0bb4527324b1e0973e89595d91c8cc102d9248525ae092e2c9b69f7414f724195b81427f28b1d3d09a51acfe354387915fd9521e8c890c125fc41a12bf34d2a1b304067ab7251e0e9ef41833ce109e76963b0b256395b16b886bca21b831f1408f836146019e7908829e716e72b81006610a2af08301de5d067c9e114a1e5759db8a6be6a3cc2806bcfe6fafd41b5bc9ddddb3dc33d6f605b1ca7d8a9e0ecdd6390d38906649e68a90a717bea80fa220170eea0c86fc78a7e10dac7b74b8e62045a3ecca54e035281fdc9fe5920a855fde3c0be522e3aef0c087524f13d973dff3768158b01a5800a060c06b451ec98d627dd052eda804d0556f60dbc490d94e6e9dea62ffcafb5beffbd9fc38fb2f0d7050004fe56b4dda0a27bc47554e1e0a7d764e17622e71f83a475db286bc7862deee1327e2028955d978272ea76bf0b88e70a18621aba59ff0c5993ef5f0e5d6b6b98e68b70203010001300d06092a864886f70d0101050500038202010079c79c8ef408a20d243d8bd8249fb9a48350dc19663b5e0fce67a8dbcb7de296c5ae7bbf72e98a2020fb78f2db29b54b0e24b181aa1c1d333cc0303685d6120b03216a913f96b96eb838f9bff125306ae3120af838c9fc07ebb5100125436bd24ec6d994d0bff5d065221871f8410daf536766757239bf594e61c5432c9817281b985263bada8381292e543a49814061ae11c92a316e7dc100327b59e3da90302c5ada68c6a50201bda1fcce800b53f381059665dbabeeb0b50eb22b2d7d2d9b0aa7488ca70e67ac6c518adb8e78454a466501e89d81a45bf1ebc350896f2c3ae4b6679ecfbf9d32960d4f5b493125c7876ef36158562371193f600bc511000a67bdb7c664d018f99d9e589868d103d7e0994f166b2ba18ff7e67d8c4da749e44dfae1d930ae5397083a51675c409049dfb626a96246c0015ca696e94ebb767a20147834bf78b07fece3f0872b057c1c519ff882501995237d8206b0b3832f78753ebd8dcbd1d3d9f5ba733538113af6b407d960ec4353c50eb38ab29888238da843cd404ed8f4952f59e4bbc0035fc77a54846a9d419179c46af1b4a3b7fc98e4d312aaa29b9b7d79e739703dc0fa41c7280d5587709277ffa11c3620f5fba985b82c238ba19b17ebd027af9424be0941719919f620dd3bb3c3f11638363708aa11f858e153cf3a69bce69978b90e4a273836100aa1e617ba455cd00426847f";
|
||||
expectedRepo.description = "The official app repository of The Guardian Project. Applications in this repository are official binaries build by the original application developers and signed by the same key as the APKs that are released in the Google Play store.";
|
||||
expectedRepo.timestamp = 1411427879;
|
||||
RepoDetails actualDetails = getFromFile("mediumRepo.xml");
|
||||
handlerTestSuite(expectedRepo, actualDetails, 15, 36, 60, 12);
|
||||
checkIncludedApps(actualDetails.apps, new String[]{
|
||||
@ -104,6 +107,7 @@ public class RepoXMLHandlerTest {
|
||||
expectedRepo.name = "F-Droid";
|
||||
expectedRepo.signingCertificate = "3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef";
|
||||
expectedRepo.description = "The official FDroid repository. Applications in this repository are mostly built directory from the source code. Some are official binaries built by the original application developers - these will be replaced by source-built versions over time.";
|
||||
expectedRepo.timestamp = 1412746769;
|
||||
RepoDetails actualDetails = getFromFile("largeRepo.xml");
|
||||
handlerTestSuite(expectedRepo, actualDetails, 1211, 2381, 14, 12);
|
||||
/*
|
||||
@ -626,6 +630,7 @@ public class RepoXMLHandlerTest {
|
||||
|
||||
assertEquals(actualDetails.maxAge, maxAge);
|
||||
assertEquals(actualDetails.version, version);
|
||||
assertEquals(expectedRepo.timestamp, actualDetails.timestamp);
|
||||
|
||||
List<App> apps = actualDetails.apps;
|
||||
assertNotNull(apps);
|
||||
@ -643,17 +648,19 @@ public class RepoXMLHandlerTest {
|
||||
public String signingCert;
|
||||
public int maxAge;
|
||||
public int version;
|
||||
public long timestamp;
|
||||
|
||||
public List<Apk> apks = new ArrayList<>();
|
||||
public List<App> apps = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void receiveRepo(String name, String description, String signingCert, int maxage, int version) {
|
||||
public void receiveRepo(String name, String description, String signingCert, int maxage, int version, long timestamp) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.signingCert = signingCert;
|
||||
this.maxAge = maxage;
|
||||
this.version = version;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -156,9 +156,9 @@ public class RepoUpdater {
|
||||
private RepoXMLHandler.IndexReceiver createIndexReceiver() {
|
||||
return new RepoXMLHandler.IndexReceiver() {
|
||||
@Override
|
||||
public void receiveRepo(String name, String description, String signingCert, int maxAge, int version) {
|
||||
public void receiveRepo(String name, String description, String signingCert, int maxAge, int version, long timestamp) {
|
||||
signingCertFromIndexXml = signingCert;
|
||||
repoDetailsToSave = prepareRepoDetailsForSaving(name, description, maxAge, version);
|
||||
repoDetailsToSave = prepareRepoDetailsForSaving(name, description, maxAge, version, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -196,6 +196,12 @@ public class RepoUpdater {
|
||||
reader.setContentHandler(repoXMLHandler);
|
||||
reader.parse(new InputSource(indexInputStream));
|
||||
|
||||
long timestamp = repoDetailsToSave.getAsLong("timestamp");
|
||||
if (timestamp < repo.timestamp) {
|
||||
throw new UpdateException(repo, "index.jar is older that current index! "
|
||||
+ timestamp + " < " + repo.timestamp);
|
||||
}
|
||||
|
||||
signingCertFromJar = getSigningCertFromJar(indexEntry);
|
||||
|
||||
// JarEntry can only read certificates after the file represented by that JarEntry
|
||||
@ -242,7 +248,7 @@ public class RepoUpdater {
|
||||
* Update tracking data for the repo represented by this instance (index version, etag,
|
||||
* description, human-readable name, etc.
|
||||
*/
|
||||
private ContentValues prepareRepoDetailsForSaving(String name, String description, int maxAge, int version) {
|
||||
private ContentValues prepareRepoDetailsForSaving(String name, String description, int maxAge, int version, long timestamp) {
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put(RepoProvider.DataColumns.LAST_UPDATED, Utils.formatTime(new Date(), ""));
|
||||
@ -269,6 +275,10 @@ public class RepoUpdater {
|
||||
values.put(RepoProvider.DataColumns.NAME, name);
|
||||
}
|
||||
|
||||
if (timestamp != repo.timestamp) {
|
||||
values.put(RepoProvider.DataColumns.TIMESTAMP, timestamp);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
// them - otherwise it will be the value specified.
|
||||
private int repoMaxAge = -1;
|
||||
private int repoVersion;
|
||||
private long repoTimestamp;
|
||||
private String repoDescription;
|
||||
private String repoName;
|
||||
|
||||
@ -60,7 +61,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
private final StringBuilder curchars = new StringBuilder();
|
||||
|
||||
interface IndexReceiver {
|
||||
void receiveRepo(String name, String description, String signingCert, int maxage, int version);
|
||||
void receiveRepo(String name, String description, String signingCert, int maxage, int version, long timestamp);
|
||||
|
||||
void receiveApp(App app, List<Apk> packages);
|
||||
}
|
||||
@ -239,7 +240,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
}
|
||||
|
||||
private void onRepoParsed() {
|
||||
receiver.receiveRepo(repoName, repoDescription, repoSigningCert, repoMaxAge, repoVersion);
|
||||
receiver.receiveRepo(repoName, repoDescription, repoSigningCert, repoMaxAge, repoVersion, repoTimestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -253,6 +254,7 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
repoVersion = Utils.parseInt(attributes.getValue("", "version"), -1);
|
||||
repoName = cleanWhiteSpace(attributes.getValue("", "name"));
|
||||
repoDescription = cleanWhiteSpace(attributes.getValue("", "description"));
|
||||
repoTimestamp = parseLong(attributes.getValue("", "timestamp"), 0);
|
||||
} else if ("application".equals(localName) && curapp == null) {
|
||||
curapp = new App();
|
||||
curapp.packageName = attributes.getValue("", "id");
|
||||
@ -271,4 +273,17 @@ public class RepoXMLHandler extends DefaultHandler {
|
||||
private static String cleanWhiteSpace(@Nullable String str) {
|
||||
return str == null ? null : str.replaceAll("\\s", " ");
|
||||
}
|
||||
|
||||
private static long parseLong(String str, long fallback) {
|
||||
if (str == null || str.length() == 0) {
|
||||
return fallback;
|
||||
}
|
||||
long result;
|
||||
try {
|
||||
result = Long.parseLong(str);
|
||||
} catch (NumberFormatException e) {
|
||||
result = fallback;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,8 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
+ "version integer not null default 0, "
|
||||
+ "lastetag text, lastUpdated string,"
|
||||
+ "isSwap integer boolean default 0,"
|
||||
+ "username string, password string"
|
||||
+ "username string, password string,"
|
||||
+ "timestamp integer not null default 0"
|
||||
+ ");";
|
||||
|
||||
private static final String CREATE_TABLE_APK =
|
||||
@ -106,7 +107,7 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
+ " );";
|
||||
private static final String DROP_TABLE_INSTALLED_APP = "DROP TABLE " + TABLE_INSTALLED_APP + ";";
|
||||
|
||||
private static final int DB_VERSION = 54;
|
||||
private static final int DB_VERSION = 55;
|
||||
|
||||
private final Context context;
|
||||
|
||||
@ -261,6 +262,7 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
values.put(RepoProvider.DataColumns.IN_USE, inUse);
|
||||
values.put(RepoProvider.DataColumns.PRIORITY, priority);
|
||||
values.put(RepoProvider.DataColumns.LAST_ETAG, (String) null);
|
||||
values.put(RepoProvider.DataColumns.TIMESTAMP, 0);
|
||||
|
||||
Utils.debugLog(TAG, "Adding repository " + name);
|
||||
db.insert(TABLE_REPO, null, values);
|
||||
@ -294,6 +296,7 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
addCredentialsToRepo(db, oldVersion);
|
||||
addAuthorToApp(db, oldVersion);
|
||||
useMaxValueInMaxSdkVersion(db, oldVersion);
|
||||
requireTimestampInRepos(db, oldVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -502,6 +505,21 @@ class DBHelper extends SQLiteOpenHelper {
|
||||
db.update(TABLE_APK, values, ApkProvider.DataColumns.MAX_SDK_VERSION + " < 1", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@code <repo timestamp="">} value was in the metadata for a long time,
|
||||
* but it was not being used in the client until now.
|
||||
*/
|
||||
private void requireTimestampInRepos(SQLiteDatabase db, int oldVersion) {
|
||||
if (oldVersion >= 55) {
|
||||
return;
|
||||
}
|
||||
if (!columnExists(db, TABLE_REPO, RepoProvider.DataColumns.TIMESTAMP)) {
|
||||
Utils.debugLog(TAG, "Adding " + RepoProvider.DataColumns.TIMESTAMP + " column to " + TABLE_REPO);
|
||||
db.execSQL("alter table " + TABLE_REPO + " add column "
|
||||
+ RepoProvider.DataColumns.TIMESTAMP + " integer not null default 0");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* By clearing the etags stored in the repo table, it means that next time the user updates
|
||||
* their repos (either manually or on a scheduled task), they will update regardless of whether
|
||||
|
@ -26,7 +26,7 @@ public class Repo extends ValueObject {
|
||||
/** The signing certificate, {@code null} for a newly added repo */
|
||||
public String signingCertificate;
|
||||
/**
|
||||
* The SHA1 fingerprint of {@link #pubkey}, set to {@code null} when a
|
||||
* The SHA1 fingerprint of {@link #signingCertificate}, set to {@code null} when a
|
||||
* newly added repo did not include fingerprint. It should never be an
|
||||
* empty {@link String}, i.e. {@code ""} */
|
||||
public String fingerprint;
|
||||
@ -40,6 +40,9 @@ public class Repo extends ValueObject {
|
||||
public String username;
|
||||
public String password;
|
||||
|
||||
/** When the signed repo index was generated, used to protect against replay attacks */
|
||||
public long timestamp;
|
||||
|
||||
public Repo() {
|
||||
}
|
||||
|
||||
@ -94,6 +97,9 @@ public class Repo extends ValueObject {
|
||||
case RepoProvider.DataColumns.PASSWORD:
|
||||
password = cursor.getString(i);
|
||||
break;
|
||||
case RepoProvider.DataColumns.TIMESTAMP:
|
||||
timestamp = cursor.getLong(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -210,5 +216,9 @@ public class Repo extends ValueObject {
|
||||
if (values.containsKey(RepoProvider.DataColumns.PASSWORD)) {
|
||||
password = values.getAsString(RepoProvider.DataColumns.PASSWORD);
|
||||
}
|
||||
|
||||
if (values.containsKey(RepoProvider.DataColumns.TIMESTAMP)) {
|
||||
timestamp = toInt(values.getAsInteger(RepoProvider.DataColumns.TIMESTAMP));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,11 +225,12 @@ public class RepoProvider extends FDroidProvider {
|
||||
String IS_SWAP = "isSwap";
|
||||
String USERNAME = "username";
|
||||
String PASSWORD = "password";
|
||||
String TIMESTAMP = "timestamp";
|
||||
|
||||
String[] ALL = {
|
||||
_ID, ADDRESS, NAME, DESCRIPTION, IN_USE, PRIORITY, SIGNING_CERT,
|
||||
FINGERPRINT, MAX_AGE, LAST_UPDATED, LAST_ETAG, VERSION, IS_SWAP,
|
||||
USERNAME, PASSWORD,
|
||||
USERNAME, PASSWORD, TIMESTAMP,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -384,6 +384,7 @@ public final class LocalRepoManager {
|
||||
serializer.attribute("", "pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate()));
|
||||
long timestamp = System.currentTimeMillis() / 1000L;
|
||||
serializer.attribute("", "timestamp", String.valueOf(timestamp));
|
||||
serializer.attribute("", "version", "10");
|
||||
|
||||
tag("description", "A local FDroid repo generated from apps installed on " + Preferences.get().getLocalRepoName());
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user