Fossil SCM
Minor security enhancements to the optional SEE integration.
Commit
a8484dc32721016061f055d2edfcb71f140d82c8
Parent
27c8985cc1b75cf…
8 files changed
+102
-13
+102
-13
+12
+33
-16
+33
-16
+57
+2
+2
M
src/db.c
+102
-13
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -870,31 +870,113 @@ | ||
| 870 | 870 | db_tolocal_function, 0, 0); |
| 871 | 871 | sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, |
| 872 | 872 | db_fromlocal_function, 0, 0); |
| 873 | 873 | } |
| 874 | 874 | |
| 875 | +#if USE_SEE | |
| 876 | +/* | |
| 877 | +** This is a pointer to the saved database encryption key string. | |
| 878 | +*/ | |
| 879 | +static char *zSavedKey = 0; | |
| 880 | + | |
| 881 | +/* | |
| 882 | +** This is the size of the saved database encryption key, in bytes. | |
| 883 | +*/ | |
| 884 | +size_t savedKeySize = 0; | |
| 885 | + | |
| 886 | +/* | |
| 887 | +** This function returns the saved database encryption key -OR- zero if | |
| 888 | +** no database encryption key is saved. | |
| 889 | +*/ | |
| 890 | +static char *db_get_saved_encryption_key(){ | |
| 891 | + return zSavedKey; | |
| 892 | +} | |
| 893 | + | |
| 894 | +/* | |
| 895 | +** This function arranges for the database encryption key to be securely | |
| 896 | +** saved in non-pagable memory (on platforms where this is possible). | |
| 897 | +*/ | |
| 898 | +static void db_save_encryption_key( | |
| 899 | + Blob *pKey | |
| 900 | +){ | |
| 901 | + void *p = NULL; | |
| 902 | + size_t n = 0; | |
| 903 | + size_t pageSize = 0; | |
| 904 | + size_t blobSize = 0; | |
| 905 | + | |
| 906 | + blobSize = blob_size(pKey); | |
| 907 | + if( blobSize==0 ) return; | |
| 908 | + fossil_get_page_size(&pageSize); | |
| 909 | + assert( pageSize>0 ); | |
| 910 | + if( blobSize>pageSize ){ | |
| 911 | + fossil_fatal("key blob too large: %u versus %u", blobSize, pageSize); | |
| 912 | + } | |
| 913 | + p = fossil_secure_alloc_page(&n); | |
| 914 | + assert( p!=NULL ); | |
| 915 | + assert( n==pageSize ); | |
| 916 | + assert( n>=blobSize ); | |
| 917 | + memcpy(p, blob_str(pKey), blobSize); | |
| 918 | + zSavedKey = p; | |
| 919 | + savedKeySize = n; | |
| 920 | +} | |
| 921 | + | |
| 922 | +/* | |
| 923 | +** This function arranges for the saved database encryption key to be | |
| 924 | +** securely zeroed, unlocked (if necessary), and freed. | |
| 925 | +*/ | |
| 926 | +void db_unsave_encryption_key(){ | |
| 927 | + fossil_secure_free_page(zSavedKey, savedKeySize); | |
| 928 | + zSavedKey = NULL; | |
| 929 | + savedKeySize = 0; | |
| 930 | +} | |
| 931 | + | |
| 932 | +/* | |
| 933 | +** This function sets the saved database encryption key to the specified | |
| 934 | +** string value, allocating or freeing the underlying memory if needed. | |
| 935 | +*/ | |
| 936 | +void db_set_saved_encryption_key( | |
| 937 | + Blob *pKey | |
| 938 | +){ | |
| 939 | + if( zSavedKey!=NULL ){ | |
| 940 | + size_t blobSize = blob_size(pKey); | |
| 941 | + if( blobSize==0 ){ | |
| 942 | + db_unsave_encryption_key(); | |
| 943 | + }else{ | |
| 944 | + if( blobSize>savedKeySize ){ | |
| 945 | + fossil_fatal("key blob too large: %u versus %u", | |
| 946 | + blobSize, savedKeySize); | |
| 947 | + } | |
| 948 | + fossil_secure_zero(zSavedKey, savedKeySize); | |
| 949 | + memcpy(zSavedKey, blob_str(pKey), blobSize); | |
| 950 | + } | |
| 951 | + }else{ | |
| 952 | + db_save_encryption_key(pKey); | |
| 953 | + } | |
| 954 | +} | |
| 955 | +#endif /* USE_SEE */ | |
| 956 | + | |
| 875 | 957 | /* |
| 876 | 958 | ** If the database file zDbFile has a name that suggests that it is |
| 877 | -** encrypted, then prompt for the encryption key and return it in the | |
| 878 | -** blob *pKey. Or, if the encryption key has previously been requested, | |
| 879 | -** just return a copy of the previous result. | |
| 959 | +** encrypted, then prompt for the database encryption key and return it | |
| 960 | +** in the blob *pKey. Or, if the encryption key has previously been | |
| 961 | +** requested, just return a copy of the previous result. The blob in | |
| 962 | +** *pKey must be initialized. | |
| 880 | 963 | */ |
| 881 | -static void db_encryption_key( | |
| 964 | +static void db_maybe_obtain_encryption_key( | |
| 882 | 965 | const char *zDbFile, /* Name of the database file */ |
| 883 | 966 | Blob *pKey /* Put the encryption key here */ |
| 884 | 967 | ){ |
| 885 | - blob_init(pKey, 0, 0); | |
| 886 | 968 | #if USE_SEE |
| 887 | 969 | if( sqlite3_strglob("*.efossil", zDbFile)==0 ){ |
| 888 | - static char *zSavedKey = 0; | |
| 889 | - if( zSavedKey ){ | |
| 890 | - blob_set(pKey, zSavedKey); | |
| 970 | + char *zKey = db_get_saved_encryption_key(); | |
| 971 | + if( zKey ){ | |
| 972 | + blob_set(pKey, zKey); | |
| 891 | 973 | }else{ |
| 892 | 974 | char *zPrompt = mprintf("\rencryption key for '%s': ", zDbFile); |
| 893 | 975 | prompt_for_password(zPrompt, pKey, 0); |
| 894 | 976 | fossil_free(zPrompt); |
| 895 | - zSavedKey = fossil_strdup(blob_str(pKey)); | |
| 977 | + db_set_saved_encryption_key(pKey); | |
| 896 | 978 | } |
| 897 | 979 | } |
| 898 | 980 | #endif |
| 899 | 981 | } |
| 900 | 982 | |
| @@ -915,14 +997,16 @@ | ||
| 915 | 997 | g.zVfsName |
| 916 | 998 | ); |
| 917 | 999 | if( rc!=SQLITE_OK ){ |
| 918 | 1000 | db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); |
| 919 | 1001 | } |
| 920 | - db_encryption_key(zDbName, &key); | |
| 1002 | + blob_init(&key, 0, 0); | |
| 1003 | + db_maybe_obtain_encryption_key(zDbName, &key); | |
| 921 | 1004 | if( blob_size(&key)>0 ){ |
| 922 | 1005 | char *zCmd = sqlite3_mprintf("PRAGMA key(%Q)", blob_str(&key)); |
| 923 | 1006 | sqlite3_exec(db, zCmd, 0, 0, 0); |
| 1007 | + fossil_secure_zero(zCmd, strlen(zCmd)); | |
| 924 | 1008 | sqlite3_free(zCmd); |
| 925 | 1009 | } |
| 926 | 1010 | blob_reset(&key); |
| 927 | 1011 | sqlite3_busy_timeout(db, 5000); |
| 928 | 1012 | sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ |
| @@ -955,14 +1039,19 @@ | ||
| 955 | 1039 | /* |
| 956 | 1040 | ** zDbName is the name of a database file. Attach zDbName using |
| 957 | 1041 | ** the name zLabel. |
| 958 | 1042 | */ |
| 959 | 1043 | void db_attach(const char *zDbName, const char *zLabel){ |
| 1044 | + char *zCmd; | |
| 960 | 1045 | Blob key; |
| 961 | - db_encryption_key(zDbName, &key); | |
| 962 | - db_multi_exec("ATTACH DATABASE %Q AS %Q KEY %Q", | |
| 963 | - zDbName, zLabel, blob_str(&key)); | |
| 1046 | + blob_init(&key, 0, 0); | |
| 1047 | + db_maybe_obtain_encryption_key(zDbName, &key); | |
| 1048 | + zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", | |
| 1049 | + zDbName, zLabel, blob_str(&key)); | |
| 1050 | + db_multi_exec(zCmd /*works-like:""*/); | |
| 1051 | + fossil_secure_zero(zCmd, strlen(zCmd)); | |
| 1052 | + sqlite3_free(zCmd); | |
| 964 | 1053 | blob_reset(&key); |
| 965 | 1054 | } |
| 966 | 1055 | |
| 967 | 1056 | /* |
| 968 | 1057 | ** Change the schema name of the "main" database to zLabel. |
| 969 | 1058 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -870,31 +870,113 @@ | |
| 870 | db_tolocal_function, 0, 0); |
| 871 | sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, |
| 872 | db_fromlocal_function, 0, 0); |
| 873 | } |
| 874 | |
| 875 | /* |
| 876 | ** If the database file zDbFile has a name that suggests that it is |
| 877 | ** encrypted, then prompt for the encryption key and return it in the |
| 878 | ** blob *pKey. Or, if the encryption key has previously been requested, |
| 879 | ** just return a copy of the previous result. |
| 880 | */ |
| 881 | static void db_encryption_key( |
| 882 | const char *zDbFile, /* Name of the database file */ |
| 883 | Blob *pKey /* Put the encryption key here */ |
| 884 | ){ |
| 885 | blob_init(pKey, 0, 0); |
| 886 | #if USE_SEE |
| 887 | if( sqlite3_strglob("*.efossil", zDbFile)==0 ){ |
| 888 | static char *zSavedKey = 0; |
| 889 | if( zSavedKey ){ |
| 890 | blob_set(pKey, zSavedKey); |
| 891 | }else{ |
| 892 | char *zPrompt = mprintf("\rencryption key for '%s': ", zDbFile); |
| 893 | prompt_for_password(zPrompt, pKey, 0); |
| 894 | fossil_free(zPrompt); |
| 895 | zSavedKey = fossil_strdup(blob_str(pKey)); |
| 896 | } |
| 897 | } |
| 898 | #endif |
| 899 | } |
| 900 | |
| @@ -915,14 +997,16 @@ | |
| 915 | g.zVfsName |
| 916 | ); |
| 917 | if( rc!=SQLITE_OK ){ |
| 918 | db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); |
| 919 | } |
| 920 | db_encryption_key(zDbName, &key); |
| 921 | if( blob_size(&key)>0 ){ |
| 922 | char *zCmd = sqlite3_mprintf("PRAGMA key(%Q)", blob_str(&key)); |
| 923 | sqlite3_exec(db, zCmd, 0, 0, 0); |
| 924 | sqlite3_free(zCmd); |
| 925 | } |
| 926 | blob_reset(&key); |
| 927 | sqlite3_busy_timeout(db, 5000); |
| 928 | sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ |
| @@ -955,14 +1039,19 @@ | |
| 955 | /* |
| 956 | ** zDbName is the name of a database file. Attach zDbName using |
| 957 | ** the name zLabel. |
| 958 | */ |
| 959 | void db_attach(const char *zDbName, const char *zLabel){ |
| 960 | Blob key; |
| 961 | db_encryption_key(zDbName, &key); |
| 962 | db_multi_exec("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 963 | zDbName, zLabel, blob_str(&key)); |
| 964 | blob_reset(&key); |
| 965 | } |
| 966 | |
| 967 | /* |
| 968 | ** Change the schema name of the "main" database to zLabel. |
| 969 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -870,31 +870,113 @@ | |
| 870 | db_tolocal_function, 0, 0); |
| 871 | sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, |
| 872 | db_fromlocal_function, 0, 0); |
| 873 | } |
| 874 | |
| 875 | #if USE_SEE |
| 876 | /* |
| 877 | ** This is a pointer to the saved database encryption key string. |
| 878 | */ |
| 879 | static char *zSavedKey = 0; |
| 880 | |
| 881 | /* |
| 882 | ** This is the size of the saved database encryption key, in bytes. |
| 883 | */ |
| 884 | size_t savedKeySize = 0; |
| 885 | |
| 886 | /* |
| 887 | ** This function returns the saved database encryption key -OR- zero if |
| 888 | ** no database encryption key is saved. |
| 889 | */ |
| 890 | static char *db_get_saved_encryption_key(){ |
| 891 | return zSavedKey; |
| 892 | } |
| 893 | |
| 894 | /* |
| 895 | ** This function arranges for the database encryption key to be securely |
| 896 | ** saved in non-pagable memory (on platforms where this is possible). |
| 897 | */ |
| 898 | static void db_save_encryption_key( |
| 899 | Blob *pKey |
| 900 | ){ |
| 901 | void *p = NULL; |
| 902 | size_t n = 0; |
| 903 | size_t pageSize = 0; |
| 904 | size_t blobSize = 0; |
| 905 | |
| 906 | blobSize = blob_size(pKey); |
| 907 | if( blobSize==0 ) return; |
| 908 | fossil_get_page_size(&pageSize); |
| 909 | assert( pageSize>0 ); |
| 910 | if( blobSize>pageSize ){ |
| 911 | fossil_fatal("key blob too large: %u versus %u", blobSize, pageSize); |
| 912 | } |
| 913 | p = fossil_secure_alloc_page(&n); |
| 914 | assert( p!=NULL ); |
| 915 | assert( n==pageSize ); |
| 916 | assert( n>=blobSize ); |
| 917 | memcpy(p, blob_str(pKey), blobSize); |
| 918 | zSavedKey = p; |
| 919 | savedKeySize = n; |
| 920 | } |
| 921 | |
| 922 | /* |
| 923 | ** This function arranges for the saved database encryption key to be |
| 924 | ** securely zeroed, unlocked (if necessary), and freed. |
| 925 | */ |
| 926 | void db_unsave_encryption_key(){ |
| 927 | fossil_secure_free_page(zSavedKey, savedKeySize); |
| 928 | zSavedKey = NULL; |
| 929 | savedKeySize = 0; |
| 930 | } |
| 931 | |
| 932 | /* |
| 933 | ** This function sets the saved database encryption key to the specified |
| 934 | ** string value, allocating or freeing the underlying memory if needed. |
| 935 | */ |
| 936 | void db_set_saved_encryption_key( |
| 937 | Blob *pKey |
| 938 | ){ |
| 939 | if( zSavedKey!=NULL ){ |
| 940 | size_t blobSize = blob_size(pKey); |
| 941 | if( blobSize==0 ){ |
| 942 | db_unsave_encryption_key(); |
| 943 | }else{ |
| 944 | if( blobSize>savedKeySize ){ |
| 945 | fossil_fatal("key blob too large: %u versus %u", |
| 946 | blobSize, savedKeySize); |
| 947 | } |
| 948 | fossil_secure_zero(zSavedKey, savedKeySize); |
| 949 | memcpy(zSavedKey, blob_str(pKey), blobSize); |
| 950 | } |
| 951 | }else{ |
| 952 | db_save_encryption_key(pKey); |
| 953 | } |
| 954 | } |
| 955 | #endif /* USE_SEE */ |
| 956 | |
| 957 | /* |
| 958 | ** If the database file zDbFile has a name that suggests that it is |
| 959 | ** encrypted, then prompt for the database encryption key and return it |
| 960 | ** in the blob *pKey. Or, if the encryption key has previously been |
| 961 | ** requested, just return a copy of the previous result. The blob in |
| 962 | ** *pKey must be initialized. |
| 963 | */ |
| 964 | static void db_maybe_obtain_encryption_key( |
| 965 | const char *zDbFile, /* Name of the database file */ |
| 966 | Blob *pKey /* Put the encryption key here */ |
| 967 | ){ |
| 968 | #if USE_SEE |
| 969 | if( sqlite3_strglob("*.efossil", zDbFile)==0 ){ |
| 970 | char *zKey = db_get_saved_encryption_key(); |
| 971 | if( zKey ){ |
| 972 | blob_set(pKey, zKey); |
| 973 | }else{ |
| 974 | char *zPrompt = mprintf("\rencryption key for '%s': ", zDbFile); |
| 975 | prompt_for_password(zPrompt, pKey, 0); |
| 976 | fossil_free(zPrompt); |
| 977 | db_set_saved_encryption_key(pKey); |
| 978 | } |
| 979 | } |
| 980 | #endif |
| 981 | } |
| 982 | |
| @@ -915,14 +997,16 @@ | |
| 997 | g.zVfsName |
| 998 | ); |
| 999 | if( rc!=SQLITE_OK ){ |
| 1000 | db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); |
| 1001 | } |
| 1002 | blob_init(&key, 0, 0); |
| 1003 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1004 | if( blob_size(&key)>0 ){ |
| 1005 | char *zCmd = sqlite3_mprintf("PRAGMA key(%Q)", blob_str(&key)); |
| 1006 | sqlite3_exec(db, zCmd, 0, 0, 0); |
| 1007 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1008 | sqlite3_free(zCmd); |
| 1009 | } |
| 1010 | blob_reset(&key); |
| 1011 | sqlite3_busy_timeout(db, 5000); |
| 1012 | sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ |
| @@ -955,14 +1039,19 @@ | |
| 1039 | /* |
| 1040 | ** zDbName is the name of a database file. Attach zDbName using |
| 1041 | ** the name zLabel. |
| 1042 | */ |
| 1043 | void db_attach(const char *zDbName, const char *zLabel){ |
| 1044 | char *zCmd; |
| 1045 | Blob key; |
| 1046 | blob_init(&key, 0, 0); |
| 1047 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1048 | zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 1049 | zDbName, zLabel, blob_str(&key)); |
| 1050 | db_multi_exec(zCmd /*works-like:""*/); |
| 1051 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1052 | sqlite3_free(zCmd); |
| 1053 | blob_reset(&key); |
| 1054 | } |
| 1055 | |
| 1056 | /* |
| 1057 | ** Change the schema name of the "main" database to zLabel. |
| 1058 |
M
src/db.c
+102
-13
| --- src/db.c | ||
| +++ src/db.c | ||
| @@ -870,31 +870,113 @@ | ||
| 870 | 870 | db_tolocal_function, 0, 0); |
| 871 | 871 | sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, |
| 872 | 872 | db_fromlocal_function, 0, 0); |
| 873 | 873 | } |
| 874 | 874 | |
| 875 | +#if USE_SEE | |
| 876 | +/* | |
| 877 | +** This is a pointer to the saved database encryption key string. | |
| 878 | +*/ | |
| 879 | +static char *zSavedKey = 0; | |
| 880 | + | |
| 881 | +/* | |
| 882 | +** This is the size of the saved database encryption key, in bytes. | |
| 883 | +*/ | |
| 884 | +size_t savedKeySize = 0; | |
| 885 | + | |
| 886 | +/* | |
| 887 | +** This function returns the saved database encryption key -OR- zero if | |
| 888 | +** no database encryption key is saved. | |
| 889 | +*/ | |
| 890 | +static char *db_get_saved_encryption_key(){ | |
| 891 | + return zSavedKey; | |
| 892 | +} | |
| 893 | + | |
| 894 | +/* | |
| 895 | +** This function arranges for the database encryption key to be securely | |
| 896 | +** saved in non-pagable memory (on platforms where this is possible). | |
| 897 | +*/ | |
| 898 | +static void db_save_encryption_key( | |
| 899 | + Blob *pKey | |
| 900 | +){ | |
| 901 | + void *p = NULL; | |
| 902 | + size_t n = 0; | |
| 903 | + size_t pageSize = 0; | |
| 904 | + size_t blobSize = 0; | |
| 905 | + | |
| 906 | + blobSize = blob_size(pKey); | |
| 907 | + if( blobSize==0 ) return; | |
| 908 | + fossil_get_page_size(&pageSize); | |
| 909 | + assert( pageSize>0 ); | |
| 910 | + if( blobSize>pageSize ){ | |
| 911 | + fossil_fatal("key blob too large: %u versus %u", blobSize, pageSize); | |
| 912 | + } | |
| 913 | + p = fossil_secure_alloc_page(&n); | |
| 914 | + assert( p!=NULL ); | |
| 915 | + assert( n==pageSize ); | |
| 916 | + assert( n>=blobSize ); | |
| 917 | + memcpy(p, blob_str(pKey), blobSize); | |
| 918 | + zSavedKey = p; | |
| 919 | + savedKeySize = n; | |
| 920 | +} | |
| 921 | + | |
| 922 | +/* | |
| 923 | +** This function arranges for the saved database encryption key to be | |
| 924 | +** securely zeroed, unlocked (if necessary), and freed. | |
| 925 | +*/ | |
| 926 | +void db_unsave_encryption_key(){ | |
| 927 | + fossil_secure_free_page(zSavedKey, savedKeySize); | |
| 928 | + zSavedKey = NULL; | |
| 929 | + savedKeySize = 0; | |
| 930 | +} | |
| 931 | + | |
| 932 | +/* | |
| 933 | +** This function sets the saved database encryption key to the specified | |
| 934 | +** string value, allocating or freeing the underlying memory if needed. | |
| 935 | +*/ | |
| 936 | +void db_set_saved_encryption_key( | |
| 937 | + Blob *pKey | |
| 938 | +){ | |
| 939 | + if( zSavedKey!=NULL ){ | |
| 940 | + size_t blobSize = blob_size(pKey); | |
| 941 | + if( blobSize==0 ){ | |
| 942 | + db_unsave_encryption_key(); | |
| 943 | + }else{ | |
| 944 | + if( blobSize>savedKeySize ){ | |
| 945 | + fossil_fatal("key blob too large: %u versus %u", | |
| 946 | + blobSize, savedKeySize); | |
| 947 | + } | |
| 948 | + fossil_secure_zero(zSavedKey, savedKeySize); | |
| 949 | + memcpy(zSavedKey, blob_str(pKey), blobSize); | |
| 950 | + } | |
| 951 | + }else{ | |
| 952 | + db_save_encryption_key(pKey); | |
| 953 | + } | |
| 954 | +} | |
| 955 | +#endif /* USE_SEE */ | |
| 956 | + | |
| 875 | 957 | /* |
| 876 | 958 | ** If the database file zDbFile has a name that suggests that it is |
| 877 | -** encrypted, then prompt for the encryption key and return it in the | |
| 878 | -** blob *pKey. Or, if the encryption key has previously been requested, | |
| 879 | -** just return a copy of the previous result. | |
| 959 | +** encrypted, then prompt for the database encryption key and return it | |
| 960 | +** in the blob *pKey. Or, if the encryption key has previously been | |
| 961 | +** requested, just return a copy of the previous result. The blob in | |
| 962 | +** *pKey must be initialized. | |
| 880 | 963 | */ |
| 881 | -static void db_encryption_key( | |
| 964 | +static void db_maybe_obtain_encryption_key( | |
| 882 | 965 | const char *zDbFile, /* Name of the database file */ |
| 883 | 966 | Blob *pKey /* Put the encryption key here */ |
| 884 | 967 | ){ |
| 885 | - blob_init(pKey, 0, 0); | |
| 886 | 968 | #if USE_SEE |
| 887 | 969 | if( sqlite3_strglob("*.efossil", zDbFile)==0 ){ |
| 888 | - static char *zSavedKey = 0; | |
| 889 | - if( zSavedKey ){ | |
| 890 | - blob_set(pKey, zSavedKey); | |
| 970 | + char *zKey = db_get_saved_encryption_key(); | |
| 971 | + if( zKey ){ | |
| 972 | + blob_set(pKey, zKey); | |
| 891 | 973 | }else{ |
| 892 | 974 | char *zPrompt = mprintf("\rencryption key for '%s': ", zDbFile); |
| 893 | 975 | prompt_for_password(zPrompt, pKey, 0); |
| 894 | 976 | fossil_free(zPrompt); |
| 895 | - zSavedKey = fossil_strdup(blob_str(pKey)); | |
| 977 | + db_set_saved_encryption_key(pKey); | |
| 896 | 978 | } |
| 897 | 979 | } |
| 898 | 980 | #endif |
| 899 | 981 | } |
| 900 | 982 | |
| @@ -915,14 +997,16 @@ | ||
| 915 | 997 | g.zVfsName |
| 916 | 998 | ); |
| 917 | 999 | if( rc!=SQLITE_OK ){ |
| 918 | 1000 | db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); |
| 919 | 1001 | } |
| 920 | - db_encryption_key(zDbName, &key); | |
| 1002 | + blob_init(&key, 0, 0); | |
| 1003 | + db_maybe_obtain_encryption_key(zDbName, &key); | |
| 921 | 1004 | if( blob_size(&key)>0 ){ |
| 922 | 1005 | char *zCmd = sqlite3_mprintf("PRAGMA key(%Q)", blob_str(&key)); |
| 923 | 1006 | sqlite3_exec(db, zCmd, 0, 0, 0); |
| 1007 | + fossil_secure_zero(zCmd, strlen(zCmd)); | |
| 924 | 1008 | sqlite3_free(zCmd); |
| 925 | 1009 | } |
| 926 | 1010 | blob_reset(&key); |
| 927 | 1011 | sqlite3_busy_timeout(db, 5000); |
| 928 | 1012 | sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ |
| @@ -955,14 +1039,19 @@ | ||
| 955 | 1039 | /* |
| 956 | 1040 | ** zDbName is the name of a database file. Attach zDbName using |
| 957 | 1041 | ** the name zLabel. |
| 958 | 1042 | */ |
| 959 | 1043 | void db_attach(const char *zDbName, const char *zLabel){ |
| 1044 | + char *zCmd; | |
| 960 | 1045 | Blob key; |
| 961 | - db_encryption_key(zDbName, &key); | |
| 962 | - db_multi_exec("ATTACH DATABASE %Q AS %Q KEY %Q", | |
| 963 | - zDbName, zLabel, blob_str(&key)); | |
| 1046 | + blob_init(&key, 0, 0); | |
| 1047 | + db_maybe_obtain_encryption_key(zDbName, &key); | |
| 1048 | + zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", | |
| 1049 | + zDbName, zLabel, blob_str(&key)); | |
| 1050 | + db_multi_exec(zCmd /*works-like:""*/); | |
| 1051 | + fossil_secure_zero(zCmd, strlen(zCmd)); | |
| 1052 | + sqlite3_free(zCmd); | |
| 964 | 1053 | blob_reset(&key); |
| 965 | 1054 | } |
| 966 | 1055 | |
| 967 | 1056 | /* |
| 968 | 1057 | ** Change the schema name of the "main" database to zLabel. |
| 969 | 1058 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -870,31 +870,113 @@ | |
| 870 | db_tolocal_function, 0, 0); |
| 871 | sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, |
| 872 | db_fromlocal_function, 0, 0); |
| 873 | } |
| 874 | |
| 875 | /* |
| 876 | ** If the database file zDbFile has a name that suggests that it is |
| 877 | ** encrypted, then prompt for the encryption key and return it in the |
| 878 | ** blob *pKey. Or, if the encryption key has previously been requested, |
| 879 | ** just return a copy of the previous result. |
| 880 | */ |
| 881 | static void db_encryption_key( |
| 882 | const char *zDbFile, /* Name of the database file */ |
| 883 | Blob *pKey /* Put the encryption key here */ |
| 884 | ){ |
| 885 | blob_init(pKey, 0, 0); |
| 886 | #if USE_SEE |
| 887 | if( sqlite3_strglob("*.efossil", zDbFile)==0 ){ |
| 888 | static char *zSavedKey = 0; |
| 889 | if( zSavedKey ){ |
| 890 | blob_set(pKey, zSavedKey); |
| 891 | }else{ |
| 892 | char *zPrompt = mprintf("\rencryption key for '%s': ", zDbFile); |
| 893 | prompt_for_password(zPrompt, pKey, 0); |
| 894 | fossil_free(zPrompt); |
| 895 | zSavedKey = fossil_strdup(blob_str(pKey)); |
| 896 | } |
| 897 | } |
| 898 | #endif |
| 899 | } |
| 900 | |
| @@ -915,14 +997,16 @@ | |
| 915 | g.zVfsName |
| 916 | ); |
| 917 | if( rc!=SQLITE_OK ){ |
| 918 | db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); |
| 919 | } |
| 920 | db_encryption_key(zDbName, &key); |
| 921 | if( blob_size(&key)>0 ){ |
| 922 | char *zCmd = sqlite3_mprintf("PRAGMA key(%Q)", blob_str(&key)); |
| 923 | sqlite3_exec(db, zCmd, 0, 0, 0); |
| 924 | sqlite3_free(zCmd); |
| 925 | } |
| 926 | blob_reset(&key); |
| 927 | sqlite3_busy_timeout(db, 5000); |
| 928 | sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ |
| @@ -955,14 +1039,19 @@ | |
| 955 | /* |
| 956 | ** zDbName is the name of a database file. Attach zDbName using |
| 957 | ** the name zLabel. |
| 958 | */ |
| 959 | void db_attach(const char *zDbName, const char *zLabel){ |
| 960 | Blob key; |
| 961 | db_encryption_key(zDbName, &key); |
| 962 | db_multi_exec("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 963 | zDbName, zLabel, blob_str(&key)); |
| 964 | blob_reset(&key); |
| 965 | } |
| 966 | |
| 967 | /* |
| 968 | ** Change the schema name of the "main" database to zLabel. |
| 969 |
| --- src/db.c | |
| +++ src/db.c | |
| @@ -870,31 +870,113 @@ | |
| 870 | db_tolocal_function, 0, 0); |
| 871 | sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0, |
| 872 | db_fromlocal_function, 0, 0); |
| 873 | } |
| 874 | |
| 875 | #if USE_SEE |
| 876 | /* |
| 877 | ** This is a pointer to the saved database encryption key string. |
| 878 | */ |
| 879 | static char *zSavedKey = 0; |
| 880 | |
| 881 | /* |
| 882 | ** This is the size of the saved database encryption key, in bytes. |
| 883 | */ |
| 884 | size_t savedKeySize = 0; |
| 885 | |
| 886 | /* |
| 887 | ** This function returns the saved database encryption key -OR- zero if |
| 888 | ** no database encryption key is saved. |
| 889 | */ |
| 890 | static char *db_get_saved_encryption_key(){ |
| 891 | return zSavedKey; |
| 892 | } |
| 893 | |
| 894 | /* |
| 895 | ** This function arranges for the database encryption key to be securely |
| 896 | ** saved in non-pagable memory (on platforms where this is possible). |
| 897 | */ |
| 898 | static void db_save_encryption_key( |
| 899 | Blob *pKey |
| 900 | ){ |
| 901 | void *p = NULL; |
| 902 | size_t n = 0; |
| 903 | size_t pageSize = 0; |
| 904 | size_t blobSize = 0; |
| 905 | |
| 906 | blobSize = blob_size(pKey); |
| 907 | if( blobSize==0 ) return; |
| 908 | fossil_get_page_size(&pageSize); |
| 909 | assert( pageSize>0 ); |
| 910 | if( blobSize>pageSize ){ |
| 911 | fossil_fatal("key blob too large: %u versus %u", blobSize, pageSize); |
| 912 | } |
| 913 | p = fossil_secure_alloc_page(&n); |
| 914 | assert( p!=NULL ); |
| 915 | assert( n==pageSize ); |
| 916 | assert( n>=blobSize ); |
| 917 | memcpy(p, blob_str(pKey), blobSize); |
| 918 | zSavedKey = p; |
| 919 | savedKeySize = n; |
| 920 | } |
| 921 | |
| 922 | /* |
| 923 | ** This function arranges for the saved database encryption key to be |
| 924 | ** securely zeroed, unlocked (if necessary), and freed. |
| 925 | */ |
| 926 | void db_unsave_encryption_key(){ |
| 927 | fossil_secure_free_page(zSavedKey, savedKeySize); |
| 928 | zSavedKey = NULL; |
| 929 | savedKeySize = 0; |
| 930 | } |
| 931 | |
| 932 | /* |
| 933 | ** This function sets the saved database encryption key to the specified |
| 934 | ** string value, allocating or freeing the underlying memory if needed. |
| 935 | */ |
| 936 | void db_set_saved_encryption_key( |
| 937 | Blob *pKey |
| 938 | ){ |
| 939 | if( zSavedKey!=NULL ){ |
| 940 | size_t blobSize = blob_size(pKey); |
| 941 | if( blobSize==0 ){ |
| 942 | db_unsave_encryption_key(); |
| 943 | }else{ |
| 944 | if( blobSize>savedKeySize ){ |
| 945 | fossil_fatal("key blob too large: %u versus %u", |
| 946 | blobSize, savedKeySize); |
| 947 | } |
| 948 | fossil_secure_zero(zSavedKey, savedKeySize); |
| 949 | memcpy(zSavedKey, blob_str(pKey), blobSize); |
| 950 | } |
| 951 | }else{ |
| 952 | db_save_encryption_key(pKey); |
| 953 | } |
| 954 | } |
| 955 | #endif /* USE_SEE */ |
| 956 | |
| 957 | /* |
| 958 | ** If the database file zDbFile has a name that suggests that it is |
| 959 | ** encrypted, then prompt for the database encryption key and return it |
| 960 | ** in the blob *pKey. Or, if the encryption key has previously been |
| 961 | ** requested, just return a copy of the previous result. The blob in |
| 962 | ** *pKey must be initialized. |
| 963 | */ |
| 964 | static void db_maybe_obtain_encryption_key( |
| 965 | const char *zDbFile, /* Name of the database file */ |
| 966 | Blob *pKey /* Put the encryption key here */ |
| 967 | ){ |
| 968 | #if USE_SEE |
| 969 | if( sqlite3_strglob("*.efossil", zDbFile)==0 ){ |
| 970 | char *zKey = db_get_saved_encryption_key(); |
| 971 | if( zKey ){ |
| 972 | blob_set(pKey, zKey); |
| 973 | }else{ |
| 974 | char *zPrompt = mprintf("\rencryption key for '%s': ", zDbFile); |
| 975 | prompt_for_password(zPrompt, pKey, 0); |
| 976 | fossil_free(zPrompt); |
| 977 | db_set_saved_encryption_key(pKey); |
| 978 | } |
| 979 | } |
| 980 | #endif |
| 981 | } |
| 982 | |
| @@ -915,14 +997,16 @@ | |
| 997 | g.zVfsName |
| 998 | ); |
| 999 | if( rc!=SQLITE_OK ){ |
| 1000 | db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); |
| 1001 | } |
| 1002 | blob_init(&key, 0, 0); |
| 1003 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1004 | if( blob_size(&key)>0 ){ |
| 1005 | char *zCmd = sqlite3_mprintf("PRAGMA key(%Q)", blob_str(&key)); |
| 1006 | sqlite3_exec(db, zCmd, 0, 0, 0); |
| 1007 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1008 | sqlite3_free(zCmd); |
| 1009 | } |
| 1010 | blob_reset(&key); |
| 1011 | sqlite3_busy_timeout(db, 5000); |
| 1012 | sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ |
| @@ -955,14 +1039,19 @@ | |
| 1039 | /* |
| 1040 | ** zDbName is the name of a database file. Attach zDbName using |
| 1041 | ** the name zLabel. |
| 1042 | */ |
| 1043 | void db_attach(const char *zDbName, const char *zLabel){ |
| 1044 | char *zCmd; |
| 1045 | Blob key; |
| 1046 | blob_init(&key, 0, 0); |
| 1047 | db_maybe_obtain_encryption_key(zDbName, &key); |
| 1048 | zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", |
| 1049 | zDbName, zLabel, blob_str(&key)); |
| 1050 | db_multi_exec(zCmd /*works-like:""*/); |
| 1051 | fossil_secure_zero(zCmd, strlen(zCmd)); |
| 1052 | sqlite3_free(zCmd); |
| 1053 | blob_reset(&key); |
| 1054 | } |
| 1055 | |
| 1056 | /* |
| 1057 | ** Change the schema name of the "main" database to zLabel. |
| 1058 |
+12
| --- src/main.c | ||
| +++ src/main.c | ||
| @@ -299,10 +299,22 @@ | ||
| 299 | 299 | /* |
| 300 | 300 | ** atexit() handler which frees up "some" of the resources |
| 301 | 301 | ** used by fossil. |
| 302 | 302 | */ |
| 303 | 303 | static void fossil_atexit(void) { |
| 304 | +#if USE_SEE | |
| 305 | + /* | |
| 306 | + ** Zero, unlock, and free the saved database encryption key now. | |
| 307 | + */ | |
| 308 | + db_unsave_encryption_key(); | |
| 309 | +#endif | |
| 310 | +#if defined(_WIN32) || defined(__BIONIC__) | |
| 311 | + /* | |
| 312 | + ** Free the secure getpass() buffer now. | |
| 313 | + */ | |
| 314 | + freepass(); | |
| 315 | +#endif | |
| 304 | 316 | #if defined(_WIN32) && !defined(_WIN64) && defined(FOSSIL_ENABLE_TCL) && \ |
| 305 | 317 | defined(USE_TCL_STUBS) |
| 306 | 318 | /* |
| 307 | 319 | ** If Tcl is compiled on Windows using the latest MinGW, Fossil can crash |
| 308 | 320 | ** when exiting while a stubs-enabled Tcl is still loaded. This is due to |
| 309 | 321 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -299,10 +299,22 @@ | |
| 299 | /* |
| 300 | ** atexit() handler which frees up "some" of the resources |
| 301 | ** used by fossil. |
| 302 | */ |
| 303 | static void fossil_atexit(void) { |
| 304 | #if defined(_WIN32) && !defined(_WIN64) && defined(FOSSIL_ENABLE_TCL) && \ |
| 305 | defined(USE_TCL_STUBS) |
| 306 | /* |
| 307 | ** If Tcl is compiled on Windows using the latest MinGW, Fossil can crash |
| 308 | ** when exiting while a stubs-enabled Tcl is still loaded. This is due to |
| 309 |
| --- src/main.c | |
| +++ src/main.c | |
| @@ -299,10 +299,22 @@ | |
| 299 | /* |
| 300 | ** atexit() handler which frees up "some" of the resources |
| 301 | ** used by fossil. |
| 302 | */ |
| 303 | static void fossil_atexit(void) { |
| 304 | #if USE_SEE |
| 305 | /* |
| 306 | ** Zero, unlock, and free the saved database encryption key now. |
| 307 | */ |
| 308 | db_unsave_encryption_key(); |
| 309 | #endif |
| 310 | #if defined(_WIN32) || defined(__BIONIC__) |
| 311 | /* |
| 312 | ** Free the secure getpass() buffer now. |
| 313 | */ |
| 314 | freepass(); |
| 315 | #endif |
| 316 | #if defined(_WIN32) && !defined(_WIN64) && defined(FOSSIL_ENABLE_TCL) && \ |
| 317 | defined(USE_TCL_STUBS) |
| 318 | /* |
| 319 | ** If Tcl is compiled on Windows using the latest MinGW, Fossil can crash |
| 320 | ** when exiting while a stubs-enabled Tcl is still loaded. This is due to |
| 321 |
+33
-16
| --- src/user.c | ||
| +++ src/user.c | ||
| @@ -19,14 +19,10 @@ | ||
| 19 | 19 | ** querying information about users. |
| 20 | 20 | */ |
| 21 | 21 | #include "config.h" |
| 22 | 22 | #include "user.h" |
| 23 | 23 | |
| 24 | -#if defined(_WIN32) | |
| 25 | -#include <conio.h> | |
| 26 | -#endif | |
| 27 | - | |
| 28 | 24 | /* |
| 29 | 25 | ** Strip leading and trailing space from a string and add the string |
| 30 | 26 | ** onto the end of a blob. |
| 31 | 27 | */ |
| 32 | 28 | static void strip_string(Blob *pBlob, char *z){ |
| @@ -43,53 +39,74 @@ | ||
| 43 | 39 | } |
| 44 | 40 | blob_append(pBlob, z, -1); |
| 45 | 41 | } |
| 46 | 42 | |
| 47 | 43 | #if defined(_WIN32) || defined(__BIONIC__) |
| 48 | -#ifdef __MINGW32__ | |
| 44 | +#ifdef _WIN32 | |
| 49 | 45 | #include <conio.h> |
| 50 | 46 | #endif |
| 47 | + | |
| 51 | 48 | /* |
| 52 | -** getpass for Windows and Android | |
| 49 | +** getpass() for Windows and Android. | |
| 53 | 50 | */ |
| 51 | +static char *zPwdBuffer = 0; | |
| 52 | +static size_t nPwdBuffer = 0; | |
| 53 | + | |
| 54 | 54 | static char *getpass(const char *prompt){ |
| 55 | - static char pwd[64]; | |
| 55 | + char *zPwd; | |
| 56 | + size_t nPwd; | |
| 56 | 57 | size_t i; |
| 57 | 58 | |
| 59 | + if( zPwdBuffer==0 ){ | |
| 60 | + zPwdBuffer = fossil_secure_alloc_page(&nPwdBuffer); | |
| 61 | + assert( zPwdBuffer ); | |
| 62 | + }else{ | |
| 63 | + fossil_secure_zero(zPwdBuffer, nPwdBuffer); | |
| 64 | + } | |
| 65 | + zPwd = zPwdBuffer; | |
| 66 | + nPwd = nPwdBuffer; | |
| 58 | 67 | fputs(prompt,stderr); |
| 59 | 68 | fflush(stderr); |
| 60 | - for(i=0; i<sizeof(pwd)-1; ++i){ | |
| 69 | + assert( zPwd!=0 ); | |
| 70 | + assert( nPwd>0 ); | |
| 71 | + for(i=0; i<nPwd-1; ++i){ | |
| 61 | 72 | #if defined(_WIN32) |
| 62 | - pwd[i] = _getch(); | |
| 73 | + zPwd[i] = _getch(); | |
| 63 | 74 | #else |
| 64 | - pwd[i] = getc(stdin); | |
| 75 | + zPwd[i] = getc(stdin); | |
| 65 | 76 | #endif |
| 66 | - if(pwd[i]=='\r' || pwd[i]=='\n'){ | |
| 77 | + if(zPwd[i]=='\r' || zPwd[i]=='\n'){ | |
| 67 | 78 | break; |
| 68 | 79 | } |
| 69 | 80 | /* BS or DEL */ |
| 70 | - else if(i>0 && (pwd[i]==8 || pwd[i]==127)){ | |
| 81 | + else if(i>0 && (zPwd[i]==8 || zPwd[i]==127)){ | |
| 71 | 82 | i -= 2; |
| 72 | 83 | continue; |
| 73 | 84 | } |
| 74 | 85 | /* CTRL-C */ |
| 75 | - else if(pwd[i]==3) { | |
| 86 | + else if(zPwd[i]==3) { | |
| 76 | 87 | i=0; |
| 77 | 88 | break; |
| 78 | 89 | } |
| 79 | 90 | /* ESC */ |
| 80 | - else if(pwd[i]==27){ | |
| 91 | + else if(zPwd[i]==27){ | |
| 81 | 92 | i=0; |
| 82 | 93 | break; |
| 83 | 94 | } |
| 84 | 95 | else{ |
| 85 | 96 | fputc('*',stderr); |
| 86 | 97 | } |
| 87 | 98 | } |
| 88 | - pwd[i]='\0'; | |
| 99 | + zPwd[i]='\0'; | |
| 89 | 100 | fputs("\n", stderr); |
| 90 | - return pwd; | |
| 101 | + assert( zPwd==zPwdBuffer ); | |
| 102 | + return zPwd; | |
| 103 | +} | |
| 104 | +void freepass(){ | |
| 105 | + if( !zPwdBuffer ) return; | |
| 106 | + assert( nPwdBuffer>0 ); | |
| 107 | + fossil_secure_free_page(zPwdBuffer, nPwdBuffer); | |
| 91 | 108 | } |
| 92 | 109 | #endif |
| 93 | 110 | |
| 94 | 111 | #if defined(_WIN32) || defined(WIN32) |
| 95 | 112 | # include <io.h> |
| 96 | 113 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -19,14 +19,10 @@ | |
| 19 | ** querying information about users. |
| 20 | */ |
| 21 | #include "config.h" |
| 22 | #include "user.h" |
| 23 | |
| 24 | #if defined(_WIN32) |
| 25 | #include <conio.h> |
| 26 | #endif |
| 27 | |
| 28 | /* |
| 29 | ** Strip leading and trailing space from a string and add the string |
| 30 | ** onto the end of a blob. |
| 31 | */ |
| 32 | static void strip_string(Blob *pBlob, char *z){ |
| @@ -43,53 +39,74 @@ | |
| 43 | } |
| 44 | blob_append(pBlob, z, -1); |
| 45 | } |
| 46 | |
| 47 | #if defined(_WIN32) || defined(__BIONIC__) |
| 48 | #ifdef __MINGW32__ |
| 49 | #include <conio.h> |
| 50 | #endif |
| 51 | /* |
| 52 | ** getpass for Windows and Android |
| 53 | */ |
| 54 | static char *getpass(const char *prompt){ |
| 55 | static char pwd[64]; |
| 56 | size_t i; |
| 57 | |
| 58 | fputs(prompt,stderr); |
| 59 | fflush(stderr); |
| 60 | for(i=0; i<sizeof(pwd)-1; ++i){ |
| 61 | #if defined(_WIN32) |
| 62 | pwd[i] = _getch(); |
| 63 | #else |
| 64 | pwd[i] = getc(stdin); |
| 65 | #endif |
| 66 | if(pwd[i]=='\r' || pwd[i]=='\n'){ |
| 67 | break; |
| 68 | } |
| 69 | /* BS or DEL */ |
| 70 | else if(i>0 && (pwd[i]==8 || pwd[i]==127)){ |
| 71 | i -= 2; |
| 72 | continue; |
| 73 | } |
| 74 | /* CTRL-C */ |
| 75 | else if(pwd[i]==3) { |
| 76 | i=0; |
| 77 | break; |
| 78 | } |
| 79 | /* ESC */ |
| 80 | else if(pwd[i]==27){ |
| 81 | i=0; |
| 82 | break; |
| 83 | } |
| 84 | else{ |
| 85 | fputc('*',stderr); |
| 86 | } |
| 87 | } |
| 88 | pwd[i]='\0'; |
| 89 | fputs("\n", stderr); |
| 90 | return pwd; |
| 91 | } |
| 92 | #endif |
| 93 | |
| 94 | #if defined(_WIN32) || defined(WIN32) |
| 95 | # include <io.h> |
| 96 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -19,14 +19,10 @@ | |
| 19 | ** querying information about users. |
| 20 | */ |
| 21 | #include "config.h" |
| 22 | #include "user.h" |
| 23 | |
| 24 | /* |
| 25 | ** Strip leading and trailing space from a string and add the string |
| 26 | ** onto the end of a blob. |
| 27 | */ |
| 28 | static void strip_string(Blob *pBlob, char *z){ |
| @@ -43,53 +39,74 @@ | |
| 39 | } |
| 40 | blob_append(pBlob, z, -1); |
| 41 | } |
| 42 | |
| 43 | #if defined(_WIN32) || defined(__BIONIC__) |
| 44 | #ifdef _WIN32 |
| 45 | #include <conio.h> |
| 46 | #endif |
| 47 | |
| 48 | /* |
| 49 | ** getpass() for Windows and Android. |
| 50 | */ |
| 51 | static char *zPwdBuffer = 0; |
| 52 | static size_t nPwdBuffer = 0; |
| 53 | |
| 54 | static char *getpass(const char *prompt){ |
| 55 | char *zPwd; |
| 56 | size_t nPwd; |
| 57 | size_t i; |
| 58 | |
| 59 | if( zPwdBuffer==0 ){ |
| 60 | zPwdBuffer = fossil_secure_alloc_page(&nPwdBuffer); |
| 61 | assert( zPwdBuffer ); |
| 62 | }else{ |
| 63 | fossil_secure_zero(zPwdBuffer, nPwdBuffer); |
| 64 | } |
| 65 | zPwd = zPwdBuffer; |
| 66 | nPwd = nPwdBuffer; |
| 67 | fputs(prompt,stderr); |
| 68 | fflush(stderr); |
| 69 | assert( zPwd!=0 ); |
| 70 | assert( nPwd>0 ); |
| 71 | for(i=0; i<nPwd-1; ++i){ |
| 72 | #if defined(_WIN32) |
| 73 | zPwd[i] = _getch(); |
| 74 | #else |
| 75 | zPwd[i] = getc(stdin); |
| 76 | #endif |
| 77 | if(zPwd[i]=='\r' || zPwd[i]=='\n'){ |
| 78 | break; |
| 79 | } |
| 80 | /* BS or DEL */ |
| 81 | else if(i>0 && (zPwd[i]==8 || zPwd[i]==127)){ |
| 82 | i -= 2; |
| 83 | continue; |
| 84 | } |
| 85 | /* CTRL-C */ |
| 86 | else if(zPwd[i]==3) { |
| 87 | i=0; |
| 88 | break; |
| 89 | } |
| 90 | /* ESC */ |
| 91 | else if(zPwd[i]==27){ |
| 92 | i=0; |
| 93 | break; |
| 94 | } |
| 95 | else{ |
| 96 | fputc('*',stderr); |
| 97 | } |
| 98 | } |
| 99 | zPwd[i]='\0'; |
| 100 | fputs("\n", stderr); |
| 101 | assert( zPwd==zPwdBuffer ); |
| 102 | return zPwd; |
| 103 | } |
| 104 | void freepass(){ |
| 105 | if( !zPwdBuffer ) return; |
| 106 | assert( nPwdBuffer>0 ); |
| 107 | fossil_secure_free_page(zPwdBuffer, nPwdBuffer); |
| 108 | } |
| 109 | #endif |
| 110 | |
| 111 | #if defined(_WIN32) || defined(WIN32) |
| 112 | # include <io.h> |
| 113 |
+33
-16
| --- src/user.c | ||
| +++ src/user.c | ||
| @@ -19,14 +19,10 @@ | ||
| 19 | 19 | ** querying information about users. |
| 20 | 20 | */ |
| 21 | 21 | #include "config.h" |
| 22 | 22 | #include "user.h" |
| 23 | 23 | |
| 24 | -#if defined(_WIN32) | |
| 25 | -#include <conio.h> | |
| 26 | -#endif | |
| 27 | - | |
| 28 | 24 | /* |
| 29 | 25 | ** Strip leading and trailing space from a string and add the string |
| 30 | 26 | ** onto the end of a blob. |
| 31 | 27 | */ |
| 32 | 28 | static void strip_string(Blob *pBlob, char *z){ |
| @@ -43,53 +39,74 @@ | ||
| 43 | 39 | } |
| 44 | 40 | blob_append(pBlob, z, -1); |
| 45 | 41 | } |
| 46 | 42 | |
| 47 | 43 | #if defined(_WIN32) || defined(__BIONIC__) |
| 48 | -#ifdef __MINGW32__ | |
| 44 | +#ifdef _WIN32 | |
| 49 | 45 | #include <conio.h> |
| 50 | 46 | #endif |
| 47 | + | |
| 51 | 48 | /* |
| 52 | -** getpass for Windows and Android | |
| 49 | +** getpass() for Windows and Android. | |
| 53 | 50 | */ |
| 51 | +static char *zPwdBuffer = 0; | |
| 52 | +static size_t nPwdBuffer = 0; | |
| 53 | + | |
| 54 | 54 | static char *getpass(const char *prompt){ |
| 55 | - static char pwd[64]; | |
| 55 | + char *zPwd; | |
| 56 | + size_t nPwd; | |
| 56 | 57 | size_t i; |
| 57 | 58 | |
| 59 | + if( zPwdBuffer==0 ){ | |
| 60 | + zPwdBuffer = fossil_secure_alloc_page(&nPwdBuffer); | |
| 61 | + assert( zPwdBuffer ); | |
| 62 | + }else{ | |
| 63 | + fossil_secure_zero(zPwdBuffer, nPwdBuffer); | |
| 64 | + } | |
| 65 | + zPwd = zPwdBuffer; | |
| 66 | + nPwd = nPwdBuffer; | |
| 58 | 67 | fputs(prompt,stderr); |
| 59 | 68 | fflush(stderr); |
| 60 | - for(i=0; i<sizeof(pwd)-1; ++i){ | |
| 69 | + assert( zPwd!=0 ); | |
| 70 | + assert( nPwd>0 ); | |
| 71 | + for(i=0; i<nPwd-1; ++i){ | |
| 61 | 72 | #if defined(_WIN32) |
| 62 | - pwd[i] = _getch(); | |
| 73 | + zPwd[i] = _getch(); | |
| 63 | 74 | #else |
| 64 | - pwd[i] = getc(stdin); | |
| 75 | + zPwd[i] = getc(stdin); | |
| 65 | 76 | #endif |
| 66 | - if(pwd[i]=='\r' || pwd[i]=='\n'){ | |
| 77 | + if(zPwd[i]=='\r' || zPwd[i]=='\n'){ | |
| 67 | 78 | break; |
| 68 | 79 | } |
| 69 | 80 | /* BS or DEL */ |
| 70 | - else if(i>0 && (pwd[i]==8 || pwd[i]==127)){ | |
| 81 | + else if(i>0 && (zPwd[i]==8 || zPwd[i]==127)){ | |
| 71 | 82 | i -= 2; |
| 72 | 83 | continue; |
| 73 | 84 | } |
| 74 | 85 | /* CTRL-C */ |
| 75 | - else if(pwd[i]==3) { | |
| 86 | + else if(zPwd[i]==3) { | |
| 76 | 87 | i=0; |
| 77 | 88 | break; |
| 78 | 89 | } |
| 79 | 90 | /* ESC */ |
| 80 | - else if(pwd[i]==27){ | |
| 91 | + else if(zPwd[i]==27){ | |
| 81 | 92 | i=0; |
| 82 | 93 | break; |
| 83 | 94 | } |
| 84 | 95 | else{ |
| 85 | 96 | fputc('*',stderr); |
| 86 | 97 | } |
| 87 | 98 | } |
| 88 | - pwd[i]='\0'; | |
| 99 | + zPwd[i]='\0'; | |
| 89 | 100 | fputs("\n", stderr); |
| 90 | - return pwd; | |
| 101 | + assert( zPwd==zPwdBuffer ); | |
| 102 | + return zPwd; | |
| 103 | +} | |
| 104 | +void freepass(){ | |
| 105 | + if( !zPwdBuffer ) return; | |
| 106 | + assert( nPwdBuffer>0 ); | |
| 107 | + fossil_secure_free_page(zPwdBuffer, nPwdBuffer); | |
| 91 | 108 | } |
| 92 | 109 | #endif |
| 93 | 110 | |
| 94 | 111 | #if defined(_WIN32) || defined(WIN32) |
| 95 | 112 | # include <io.h> |
| 96 | 113 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -19,14 +19,10 @@ | |
| 19 | ** querying information about users. |
| 20 | */ |
| 21 | #include "config.h" |
| 22 | #include "user.h" |
| 23 | |
| 24 | #if defined(_WIN32) |
| 25 | #include <conio.h> |
| 26 | #endif |
| 27 | |
| 28 | /* |
| 29 | ** Strip leading and trailing space from a string and add the string |
| 30 | ** onto the end of a blob. |
| 31 | */ |
| 32 | static void strip_string(Blob *pBlob, char *z){ |
| @@ -43,53 +39,74 @@ | |
| 43 | } |
| 44 | blob_append(pBlob, z, -1); |
| 45 | } |
| 46 | |
| 47 | #if defined(_WIN32) || defined(__BIONIC__) |
| 48 | #ifdef __MINGW32__ |
| 49 | #include <conio.h> |
| 50 | #endif |
| 51 | /* |
| 52 | ** getpass for Windows and Android |
| 53 | */ |
| 54 | static char *getpass(const char *prompt){ |
| 55 | static char pwd[64]; |
| 56 | size_t i; |
| 57 | |
| 58 | fputs(prompt,stderr); |
| 59 | fflush(stderr); |
| 60 | for(i=0; i<sizeof(pwd)-1; ++i){ |
| 61 | #if defined(_WIN32) |
| 62 | pwd[i] = _getch(); |
| 63 | #else |
| 64 | pwd[i] = getc(stdin); |
| 65 | #endif |
| 66 | if(pwd[i]=='\r' || pwd[i]=='\n'){ |
| 67 | break; |
| 68 | } |
| 69 | /* BS or DEL */ |
| 70 | else if(i>0 && (pwd[i]==8 || pwd[i]==127)){ |
| 71 | i -= 2; |
| 72 | continue; |
| 73 | } |
| 74 | /* CTRL-C */ |
| 75 | else if(pwd[i]==3) { |
| 76 | i=0; |
| 77 | break; |
| 78 | } |
| 79 | /* ESC */ |
| 80 | else if(pwd[i]==27){ |
| 81 | i=0; |
| 82 | break; |
| 83 | } |
| 84 | else{ |
| 85 | fputc('*',stderr); |
| 86 | } |
| 87 | } |
| 88 | pwd[i]='\0'; |
| 89 | fputs("\n", stderr); |
| 90 | return pwd; |
| 91 | } |
| 92 | #endif |
| 93 | |
| 94 | #if defined(_WIN32) || defined(WIN32) |
| 95 | # include <io.h> |
| 96 |
| --- src/user.c | |
| +++ src/user.c | |
| @@ -19,14 +19,10 @@ | |
| 19 | ** querying information about users. |
| 20 | */ |
| 21 | #include "config.h" |
| 22 | #include "user.h" |
| 23 | |
| 24 | /* |
| 25 | ** Strip leading and trailing space from a string and add the string |
| 26 | ** onto the end of a blob. |
| 27 | */ |
| 28 | static void strip_string(Blob *pBlob, char *z){ |
| @@ -43,53 +39,74 @@ | |
| 39 | } |
| 40 | blob_append(pBlob, z, -1); |
| 41 | } |
| 42 | |
| 43 | #if defined(_WIN32) || defined(__BIONIC__) |
| 44 | #ifdef _WIN32 |
| 45 | #include <conio.h> |
| 46 | #endif |
| 47 | |
| 48 | /* |
| 49 | ** getpass() for Windows and Android. |
| 50 | */ |
| 51 | static char *zPwdBuffer = 0; |
| 52 | static size_t nPwdBuffer = 0; |
| 53 | |
| 54 | static char *getpass(const char *prompt){ |
| 55 | char *zPwd; |
| 56 | size_t nPwd; |
| 57 | size_t i; |
| 58 | |
| 59 | if( zPwdBuffer==0 ){ |
| 60 | zPwdBuffer = fossil_secure_alloc_page(&nPwdBuffer); |
| 61 | assert( zPwdBuffer ); |
| 62 | }else{ |
| 63 | fossil_secure_zero(zPwdBuffer, nPwdBuffer); |
| 64 | } |
| 65 | zPwd = zPwdBuffer; |
| 66 | nPwd = nPwdBuffer; |
| 67 | fputs(prompt,stderr); |
| 68 | fflush(stderr); |
| 69 | assert( zPwd!=0 ); |
| 70 | assert( nPwd>0 ); |
| 71 | for(i=0; i<nPwd-1; ++i){ |
| 72 | #if defined(_WIN32) |
| 73 | zPwd[i] = _getch(); |
| 74 | #else |
| 75 | zPwd[i] = getc(stdin); |
| 76 | #endif |
| 77 | if(zPwd[i]=='\r' || zPwd[i]=='\n'){ |
| 78 | break; |
| 79 | } |
| 80 | /* BS or DEL */ |
| 81 | else if(i>0 && (zPwd[i]==8 || zPwd[i]==127)){ |
| 82 | i -= 2; |
| 83 | continue; |
| 84 | } |
| 85 | /* CTRL-C */ |
| 86 | else if(zPwd[i]==3) { |
| 87 | i=0; |
| 88 | break; |
| 89 | } |
| 90 | /* ESC */ |
| 91 | else if(zPwd[i]==27){ |
| 92 | i=0; |
| 93 | break; |
| 94 | } |
| 95 | else{ |
| 96 | fputc('*',stderr); |
| 97 | } |
| 98 | } |
| 99 | zPwd[i]='\0'; |
| 100 | fputs("\n", stderr); |
| 101 | assert( zPwd==zPwdBuffer ); |
| 102 | return zPwd; |
| 103 | } |
| 104 | void freepass(){ |
| 105 | if( !zPwdBuffer ) return; |
| 106 | assert( nPwdBuffer>0 ); |
| 107 | fossil_secure_free_page(zPwdBuffer, nPwdBuffer); |
| 108 | } |
| 109 | #endif |
| 110 | |
| 111 | #if defined(_WIN32) || defined(WIN32) |
| 112 | # include <io.h> |
| 113 |
+57
| --- src/util.c | ||
| +++ src/util.c | ||
| @@ -55,10 +55,67 @@ | ||
| 55 | 55 | } |
| 56 | 56 | void *fossil_realloc(void *p, size_t n){ |
| 57 | 57 | p = realloc(p, n); |
| 58 | 58 | if( p==0 ) fossil_panic("out of memory"); |
| 59 | 59 | return p; |
| 60 | +} | |
| 61 | +void fossil_secure_zero(void *p, size_t n){ | |
| 62 | + volatile unsigned char *vp = (volatile unsigned char *)p; | |
| 63 | + size_t i; | |
| 64 | + | |
| 65 | + if( p==0 ) return; | |
| 66 | + assert( n>0 ); | |
| 67 | + if( n==0 ) return; | |
| 68 | + for(i=0; i<n; i++){ vp[i] ^= 0xFF; } | |
| 69 | + for(i=0; i<n; i++){ vp[i] ^= vp[i]; } | |
| 70 | +} | |
| 71 | +void fossil_get_page_size(size_t *piPageSize){ | |
| 72 | +#if defined(_WIN32) | |
| 73 | + SYSTEM_INFO sysInfo; | |
| 74 | + memset(&sysInfo, 0, sizeof(SYSTEM_INFO)); | |
| 75 | + GetSystemInfo(&sysInfo); | |
| 76 | + *piPageSize = (size_t)sysInfo.dwPageSize; | |
| 77 | +#else | |
| 78 | + *piPageSize = 4096; /* FIXME: What for POSIX? */ | |
| 79 | +#endif | |
| 80 | +} | |
| 81 | +void *fossil_secure_alloc_page(size_t *pN){ | |
| 82 | + void *p; | |
| 83 | + size_t pageSize; | |
| 84 | + | |
| 85 | + fossil_get_page_size(&pageSize); | |
| 86 | + assert( pageSize>0 ); | |
| 87 | + assert( pageSize%2==0 ); | |
| 88 | +#if defined(_WIN32) | |
| 89 | + p = VirtualAlloc(NULL, pageSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); | |
| 90 | + if( p==NULL ){ | |
| 91 | + fossil_fatal("VirtualAlloc failed: %lu\n", GetLastError()); | |
| 92 | + } | |
| 93 | + if( !VirtualLock(p, pageSize) ){ | |
| 94 | + fossil_fatal("VirtualLock failed: %lu\n", GetLastError()); | |
| 95 | + } | |
| 96 | +#else | |
| 97 | + p = fossil_malloc(pageSize); | |
| 98 | +#endif | |
| 99 | + fossil_secure_zero(p, pageSize); | |
| 100 | + if( pN ) *pN = pageSize; | |
| 101 | + return p; | |
| 102 | +} | |
| 103 | +void fossil_secure_free_page(void *p, size_t n){ | |
| 104 | + if( !p ) return; | |
| 105 | + assert( n>0 ); | |
| 106 | + fossil_secure_zero(p, n); | |
| 107 | +#if defined(_WIN32) | |
| 108 | + if( !VirtualUnlock(p, n) ){ | |
| 109 | + fossil_fatal("VirtualUnlock failed: %lu\n", GetLastError()); | |
| 110 | + } | |
| 111 | + if( !VirtualFree(p, 0, MEM_RELEASE) ){ | |
| 112 | + fossil_fatal("VirtualFree failed: %lu\n", GetLastError()); | |
| 113 | + } | |
| 114 | +#else | |
| 115 | + fossil_free(p); | |
| 116 | +#endif | |
| 60 | 117 | } |
| 61 | 118 | |
| 62 | 119 | /* |
| 63 | 120 | ** This function implements a cross-platform "system()" interface. |
| 64 | 121 | */ |
| 65 | 122 |
| --- src/util.c | |
| +++ src/util.c | |
| @@ -55,10 +55,67 @@ | |
| 55 | } |
| 56 | void *fossil_realloc(void *p, size_t n){ |
| 57 | p = realloc(p, n); |
| 58 | if( p==0 ) fossil_panic("out of memory"); |
| 59 | return p; |
| 60 | } |
| 61 | |
| 62 | /* |
| 63 | ** This function implements a cross-platform "system()" interface. |
| 64 | */ |
| 65 |
| --- src/util.c | |
| +++ src/util.c | |
| @@ -55,10 +55,67 @@ | |
| 55 | } |
| 56 | void *fossil_realloc(void *p, size_t n){ |
| 57 | p = realloc(p, n); |
| 58 | if( p==0 ) fossil_panic("out of memory"); |
| 59 | return p; |
| 60 | } |
| 61 | void fossil_secure_zero(void *p, size_t n){ |
| 62 | volatile unsigned char *vp = (volatile unsigned char *)p; |
| 63 | size_t i; |
| 64 | |
| 65 | if( p==0 ) return; |
| 66 | assert( n>0 ); |
| 67 | if( n==0 ) return; |
| 68 | for(i=0; i<n; i++){ vp[i] ^= 0xFF; } |
| 69 | for(i=0; i<n; i++){ vp[i] ^= vp[i]; } |
| 70 | } |
| 71 | void fossil_get_page_size(size_t *piPageSize){ |
| 72 | #if defined(_WIN32) |
| 73 | SYSTEM_INFO sysInfo; |
| 74 | memset(&sysInfo, 0, sizeof(SYSTEM_INFO)); |
| 75 | GetSystemInfo(&sysInfo); |
| 76 | *piPageSize = (size_t)sysInfo.dwPageSize; |
| 77 | #else |
| 78 | *piPageSize = 4096; /* FIXME: What for POSIX? */ |
| 79 | #endif |
| 80 | } |
| 81 | void *fossil_secure_alloc_page(size_t *pN){ |
| 82 | void *p; |
| 83 | size_t pageSize; |
| 84 | |
| 85 | fossil_get_page_size(&pageSize); |
| 86 | assert( pageSize>0 ); |
| 87 | assert( pageSize%2==0 ); |
| 88 | #if defined(_WIN32) |
| 89 | p = VirtualAlloc(NULL, pageSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); |
| 90 | if( p==NULL ){ |
| 91 | fossil_fatal("VirtualAlloc failed: %lu\n", GetLastError()); |
| 92 | } |
| 93 | if( !VirtualLock(p, pageSize) ){ |
| 94 | fossil_fatal("VirtualLock failed: %lu\n", GetLastError()); |
| 95 | } |
| 96 | #else |
| 97 | p = fossil_malloc(pageSize); |
| 98 | #endif |
| 99 | fossil_secure_zero(p, pageSize); |
| 100 | if( pN ) *pN = pageSize; |
| 101 | return p; |
| 102 | } |
| 103 | void fossil_secure_free_page(void *p, size_t n){ |
| 104 | if( !p ) return; |
| 105 | assert( n>0 ); |
| 106 | fossil_secure_zero(p, n); |
| 107 | #if defined(_WIN32) |
| 108 | if( !VirtualUnlock(p, n) ){ |
| 109 | fossil_fatal("VirtualUnlock failed: %lu\n", GetLastError()); |
| 110 | } |
| 111 | if( !VirtualFree(p, 0, MEM_RELEASE) ){ |
| 112 | fossil_fatal("VirtualFree failed: %lu\n", GetLastError()); |
| 113 | } |
| 114 | #else |
| 115 | fossil_free(p); |
| 116 | #endif |
| 117 | } |
| 118 | |
| 119 | /* |
| 120 | ** This function implements a cross-platform "system()" interface. |
| 121 | */ |
| 122 |
+2
| --- www/changes.wiki | ||
| +++ www/changes.wiki | ||
| @@ -8,10 +8,12 @@ | ||
| 8 | 8 | from the "ticketchng" table. |
| 9 | 9 | * Enhance the "brlist" page to make use of branch colors. |
| 10 | 10 | * Remove the "fusefs" command from builds that do not have the underlying |
| 11 | 11 | support enabled. |
| 12 | 12 | * Fixes for incremental git import/export. |
| 13 | + * Minor security enhancements to | |
| 14 | + [./encryptedrepos.wiki|encrypted repositories]. | |
| 13 | 15 | * TH1 enhancements: |
| 14 | 16 | <ul><li>Add <nowiki>[unversioned content]</nowiki> command.</li> |
| 15 | 17 | <li>Add <nowiki>[unversioned list]</nowiki> command.</li> |
| 16 | 18 | <li>Add project_description variable.</li> |
| 17 | 19 | </ul> |
| 18 | 20 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -8,10 +8,12 @@ | |
| 8 | from the "ticketchng" table. |
| 9 | * Enhance the "brlist" page to make use of branch colors. |
| 10 | * Remove the "fusefs" command from builds that do not have the underlying |
| 11 | support enabled. |
| 12 | * Fixes for incremental git import/export. |
| 13 | * TH1 enhancements: |
| 14 | <ul><li>Add <nowiki>[unversioned content]</nowiki> command.</li> |
| 15 | <li>Add <nowiki>[unversioned list]</nowiki> command.</li> |
| 16 | <li>Add project_description variable.</li> |
| 17 | </ul> |
| 18 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -8,10 +8,12 @@ | |
| 8 | from the "ticketchng" table. |
| 9 | * Enhance the "brlist" page to make use of branch colors. |
| 10 | * Remove the "fusefs" command from builds that do not have the underlying |
| 11 | support enabled. |
| 12 | * Fixes for incremental git import/export. |
| 13 | * Minor security enhancements to |
| 14 | [./encryptedrepos.wiki|encrypted repositories]. |
| 15 | * TH1 enhancements: |
| 16 | <ul><li>Add <nowiki>[unversioned content]</nowiki> command.</li> |
| 17 | <li>Add <nowiki>[unversioned list]</nowiki> command.</li> |
| 18 | <li>Add project_description variable.</li> |
| 19 | </ul> |
| 20 |
+2
| --- www/changes.wiki | ||
| +++ www/changes.wiki | ||
| @@ -8,10 +8,12 @@ | ||
| 8 | 8 | from the "ticketchng" table. |
| 9 | 9 | * Enhance the "brlist" page to make use of branch colors. |
| 10 | 10 | * Remove the "fusefs" command from builds that do not have the underlying |
| 11 | 11 | support enabled. |
| 12 | 12 | * Fixes for incremental git import/export. |
| 13 | + * Minor security enhancements to | |
| 14 | + [./encryptedrepos.wiki|encrypted repositories]. | |
| 13 | 15 | * TH1 enhancements: |
| 14 | 16 | <ul><li>Add <nowiki>[unversioned content]</nowiki> command.</li> |
| 15 | 17 | <li>Add <nowiki>[unversioned list]</nowiki> command.</li> |
| 16 | 18 | <li>Add project_description variable.</li> |
| 17 | 19 | </ul> |
| 18 | 20 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -8,10 +8,12 @@ | |
| 8 | from the "ticketchng" table. |
| 9 | * Enhance the "brlist" page to make use of branch colors. |
| 10 | * Remove the "fusefs" command from builds that do not have the underlying |
| 11 | support enabled. |
| 12 | * Fixes for incremental git import/export. |
| 13 | * TH1 enhancements: |
| 14 | <ul><li>Add <nowiki>[unversioned content]</nowiki> command.</li> |
| 15 | <li>Add <nowiki>[unversioned list]</nowiki> command.</li> |
| 16 | <li>Add project_description variable.</li> |
| 17 | </ul> |
| 18 |
| --- www/changes.wiki | |
| +++ www/changes.wiki | |
| @@ -8,10 +8,12 @@ | |
| 8 | from the "ticketchng" table. |
| 9 | * Enhance the "brlist" page to make use of branch colors. |
| 10 | * Remove the "fusefs" command from builds that do not have the underlying |
| 11 | support enabled. |
| 12 | * Fixes for incremental git import/export. |
| 13 | * Minor security enhancements to |
| 14 | [./encryptedrepos.wiki|encrypted repositories]. |
| 15 | * TH1 enhancements: |
| 16 | <ul><li>Add <nowiki>[unversioned content]</nowiki> command.</li> |
| 17 | <li>Add <nowiki>[unversioned list]</nowiki> command.</li> |
| 18 | <li>Add project_description variable.</li> |
| 19 | </ul> |
| 20 |