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:
Hans-Christoph Steiner 2016-05-19 12:30:44 +02:00
parent 014ab2d2b6
commit 02b2090e53
8 changed files with 95 additions and 13 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}
@ -79,7 +80,7 @@ public class RepoXMLHandler extends DefaultHandler {
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException {
throws SAXException {
if ("application".equals(localName) && curapp != null) {
onApplicationParsed();
@ -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;
}
}

View File

@ -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

View File

@ -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));
}
}
}

View File

@ -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,
};
}

View File

@ -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());