#include "xdatabase.h"
#include "agentquery.h"
#include "key.h"
#include "server.h"
#include "session.h"
#include "cfnotifier.h" // legacy
#include "notifications.h"
#include "SecurityAgentClient.h"
#include <Security/acl_any.h> // for default owner ACLs
#include <Security/wrapkey.h>
#include <Security/endian.h>
Database::Database(const DLDbIdentifier &id, const DBParameters ¶ms, Process &proc,
const AccessCredentials *cred, const AclEntryPrototype *owner)
: SecurityServerAcl(dbAcl, CssmAllocator::standard()), process(proc),
mValidData(false), version(0), mBlob(NULL)
{
mCred = DataWalkers::copy(cred, CssmAllocator::standard());
Signature newSig;
Server::active().random(newSig.bytes);
DbIdentifier ident(id, newSig);
CommonMap &commons = proc.session.databases();
common = new Common(ident, commons);
StLock<Mutex> _(*common);
{ StLock<Mutex> _(commons);
assert(commons.find(ident) == commons.end()); commons[ident] = common;
common->useCount++;
}
establishNewSecrets(cred, SecurityAgent::newDatabase);
common->mParams = params;
common->makeNewSecrets();
if (owner)
cssmSetInitial(*owner);
else
cssmSetInitial(new AnyAclSubject());
mValidData = true;
encode();
process.addDatabase(this);
secdebug("SSdb", "database %s(%p) created, common at %p",
common->dbName(), this, common);
IFDUMPING("SSdb", debugDump("creation complete"));
}
Database::Database(const DLDbIdentifier &id, const DbBlob *blob, Process &proc,
const AccessCredentials *cred)
: SecurityServerAcl(dbAcl, CssmAllocator::standard()), process(proc),
mValidData(false), version(0)
{
assert(blob);
blob->validate(CSSMERR_APPLEDL_INVALID_DATABASE_BLOB);
switch (blob->version()) {
#if defined(COMPAT_OSX_10_0)
case blob->version_MacOS_10_0:
break;
#endif
case blob->version_MacOS_10_1:
break;
default:
CssmError::throwMe(CSSMERR_APPLEDL_INCOMPATIBLE_DATABASE_BLOB);
}
mCred = DataWalkers::copy(cred, CssmAllocator::standard());
DbIdentifier ident(id, blob->randomSignature);
CommonMap &commons = proc.session.databases();
StLock<Mutex> mapLock(commons);
CommonMap::iterator it = commons.find(ident);
if (it != commons.end()) {
common = it->second; StLock<Mutex> _(*common); common->useCount++;
secdebug("SSdb",
"open database %s(%p) version %lx at known common %p(%d)",
common->dbName(), this, blob->version(), common, int(common->useCount));
} else {
commons[ident] = common = new Common(ident, commons);
common->mParams = blob->params;
common->useCount++;
secdebug("SSdb", "open database %s(%p) version %lx with new common %p",
common->dbName(), this, blob->version(), common);
}
process.addDatabase(this);
mBlob = blob->copy();
IFDUMPING("SSdb", debugDump("end of decode"));
}
Database::~Database()
{
secdebug("SSdb", "deleting database %s(%p) common %p (%d refs)",
common->dbName(), this, common, int(common->useCount));
IFDUMPING("SSdb", debugDump("deleting database instance"));
process.removeDatabase(this);
CssmAllocator::standard().free(mCred);
CssmAllocator::standard().free(mBlob);
CommonMap &commons = process.session.databases();
StLock<Mutex> __(commons);
if (--common->useCount == 0 && common->isLocked()) {
IFDUMPING("SSdb", debugDump("discarding common"));
discard(common);
} else if (common->useCount == 0)
IFDUMPING("SSdb", debugDump("retained because it's unlocked"));
}
void Database::authenticate(const AccessCredentials *cred)
{
StLock<Mutex> _(*common);
AccessCredentials *newCred = DataWalkers::copy(cred, CssmAllocator::standard());
CssmAllocator::standard().free(mCred);
mCred = newCred;
}
DbBlob *Database::blob()
{
StLock<Mutex> _(*common);
if (!validBlob()) {
makeUnlocked(); encode(); }
activity(); assert(validBlob()); return mBlob;
}
void Database::encode()
{
DbBlob *blob = common->encode(*this);
CssmAllocator::standard().free(mBlob);
mBlob = blob;
version = common->version;
secdebug("SSdb", "encoded database %p common %p(%s) version %ld params=(%ld,%d)",
this, common, dbName(), version,
common->mParams.idleTimeout, common->mParams.lockOnSleep);
}
void Database::changePassphrase(const AccessCredentials *cred)
{
StLock<Mutex> _(*common);
makeUnlocked(cred);
establishNewSecrets(cred, SecurityAgent::changePassphrase);
common->version++; secdebug("SSdb", "Database %s(%p) master secret changed", common->dbName(), this);
encode();
KeychainNotifier::passphraseChanged(identifier());
activity();
}
Key *Database::extractMasterKey(Database *db,
const AccessCredentials *cred, const AclEntryPrototype *owner,
uint32 usage, uint32 attrs)
{
StLock<Mutex> _(*common);
makeUnlocked(cred);
CssmClient::WrapKey wrap(Server::csp(), CSSM_ALGID_NONE);
CssmKey key;
wrap(common->masterKey(), key);
return new Key(db, key, attrs & Key::managedAttributes, owner);
}
void Database::getDbIndex(CssmData &indexData)
{
if (!mBlob)
encode(); assert(mBlob);
CssmData signature = CssmData::wrap(mBlob->randomSignature);
indexData = CssmAutoData(CssmAllocator::standard(), signature).release();
}
void Database::unlock()
{
StLock<Mutex> _(*common);
makeUnlocked();
}
void Database::makeUnlocked()
{
return makeUnlocked(mCred);
}
void Database::makeUnlocked(const AccessCredentials *cred)
{
IFDUMPING("SSdb", debugDump("default procedures unlock"));
if (isLocked()) {
assert(mBlob || (mValidData && common->hasMaster()));
establishOldSecrets(cred);
activity(); } else if (!mValidData) { if (!decode())
CssmError::throwMe(CSSM_ERRCODE_OPERATION_AUTH_DENIED);
}
assert(!isLocked());
assert(mValidData);
}
void Database::unlock(const CssmData &passphrase)
{
StLock<Mutex> _(*common);
makeUnlocked(passphrase);
}
void Database::makeUnlocked(const CssmData &passphrase)
{
if (isLocked()) {
if (decode(passphrase))
return;
else
CssmError::throwMe(CSSM_ERRCODE_OPERATION_AUTH_DENIED);
} else if (!mValidData) { if (!decode())
CssmError::throwMe(CSSM_ERRCODE_OPERATION_AUTH_DENIED);
}
assert(!isLocked());
assert(mValidData);
}
bool Database::decode(const CssmData &passphrase)
{
assert(mBlob);
common->setup(mBlob, passphrase);
return decode();
}
bool Database::decode()
{
assert(mBlob);
assert(common->hasMaster());
void *privateAclBlob;
if (common->unlock(mBlob, &privateAclBlob)) {
if (!mValidData) {
importBlob(mBlob->publicAclBlob(), privateAclBlob);
mValidData = true;
}
CssmAllocator::standard().free(privateAclBlob);
return true;
}
secdebug("SSdb", "%p decode failed", this);
return false;
}
void Database::establishOldSecrets(const AccessCredentials *creds)
{
list<CssmSample> samples;
if (creds && creds->samples().collect(CSSM_SAMPLE_TYPE_KEYCHAIN_LOCK, samples)) {
for (list<CssmSample>::iterator it = samples.begin(); it != samples.end(); it++) {
TypedList &sample = *it;
sample.checkProper();
switch (sample.type()) {
case CSSM_SAMPLE_TYPE_KEYCHAIN_PROMPT:
{
secdebug("SSdb", "%p attempting interactive unlock", this);
QueryUnlock query(*this);
if (query() == SecurityAgent::noReason)
return;
}
break;
case CSSM_SAMPLE_TYPE_PASSWORD:
if (sample.length() != 2)
CssmError::throwMe(CSSM_ERRCODE_INVALID_SAMPLE_VALUE);
secdebug("SSdb", "%p attempting passphrase unlock", this);
if (decode(sample[1]))
return;
break;
case CSSM_WORDID_SYMMETRIC_KEY:
assert(mBlob);
secdebug("SSdb", "%p attempting explicit key unlock", this);
common->setup(mBlob, keyFromCreds(sample));
if (decode())
return;
break;
case CSSM_WORDID_CANCELED:
secdebug("SSdb", "%p defeat default action", this);
break;
default:
secdebug("SSdb", "%p unknown sub-sample unlock (%ld) ignored", this, sample.type());
break;
}
}
} else {
assert(mBlob);
SystemKeychainKey systemKeychain(kSystemUnlockFile);
if (systemKeychain.matches(mBlob->randomSignature)) {
secdebug("SSdb", "%p attempting system unlock", this);
common->setup(mBlob, CssmClient::Key(Server::csp(), systemKeychain.key(), true));
if (decode())
return;
}
QueryUnlock query(*this);
if (query() == SecurityAgent::noReason)
return;
}
CssmError::throwMe(CSSM_ERRCODE_OPERATION_AUTH_DENIED);
}
void Database::establishNewSecrets(const AccessCredentials *creds, SecurityAgent::Reason reason)
{
list<CssmSample> samples;
if (creds && creds->samples().collect(CSSM_SAMPLE_TYPE_KEYCHAIN_CHANGE_LOCK, samples)) {
for (list<CssmSample>::iterator it = samples.begin(); it != samples.end(); it++) {
TypedList &sample = *it;
sample.checkProper();
switch (sample.type()) {
case CSSM_SAMPLE_TYPE_KEYCHAIN_PROMPT:
{
secdebug("SSdb", "%p specified interactive passphrase", this);
QueryNewPassphrase query(*this, reason);
CssmAutoData passphrase(CssmAllocator::standard(CssmAllocator::sensitive));
if (query(passphrase) == SecurityAgent::noReason) {
common->setup(NULL, passphrase);
return;
}
}
break;
case CSSM_SAMPLE_TYPE_PASSWORD:
secdebug("SSdb", "%p specified explicit passphrase", this);
if (sample.length() != 2)
CssmError::throwMe(CSSM_ERRCODE_INVALID_SAMPLE_VALUE);
common->setup(NULL, sample[1]);
return;
case CSSM_WORDID_SYMMETRIC_KEY:
secdebug("SSdb", "%p specified explicit master key", this);
common->setup(NULL, keyFromCreds(sample));
return;
case CSSM_WORDID_CANCELED:
secdebug("SSdb", "%p defeat default action", this);
break;
default:
secdebug("SSdb", "%p unknown sub-sample acquisition (%ld) ignored",
this, sample.type());
break;
}
}
} else {
QueryNewPassphrase query(*this, reason);
CssmAutoData passphrase(CssmAllocator::standard(CssmAllocator::sensitive));
if (query(passphrase) == SecurityAgent::noReason) {
common->setup(NULL, passphrase);
return;
}
}
CssmError::throwMe(CSSM_ERRCODE_OPERATION_AUTH_DENIED);
}
CssmClient::Key Database::keyFromCreds(const TypedList &sample)
{
assert(sample.type() == CSSM_WORDID_SYMMETRIC_KEY);
if (sample.length() != 3
|| sample[1].type() != CSSM_LIST_ELEMENT_DATUM
|| sample[2].type() != CSSM_LIST_ELEMENT_DATUM)
CssmError::throwMe(CSSM_ERRCODE_INVALID_SAMPLE_VALUE);
CSSM_CSP_HANDLE &handle = *sample[1].data().interpretedAs<CSSM_CSP_HANDLE>(CSSM_ERRCODE_INVALID_SAMPLE_VALUE);
CssmKey &key = *sample[2].data().interpretedAs<CssmKey>(CSSM_ERRCODE_INVALID_SAMPLE_VALUE);
if (key.header().cspGuid() == gGuidAppleCSPDL) {
return Server::key(handle);
} else {
if (key.header().blobType() != CSSM_KEYBLOB_RAW)
CssmError::throwMe(CSSMERR_CSP_INVALID_KEY_REFERENCE);
if (key.header().keyClass() != CSSM_KEYCLASS_SESSION_KEY)
CssmError::throwMe(CSSMERR_CSP_INVALID_KEY_CLASS);
return CssmClient::Key(Server::csp(), key, true);
}
}
bool Database::validatePassphrase(const CssmData &passphrase) const
{
if (common->hasMaster()) {
return common->validatePassphrase(passphrase);
} else {
try {
DatabaseCryptoCore test;
test.setup(mBlob, passphrase);
test.decodeCore(mBlob, NULL);
return true;
} catch (...) {
return false;
}
}
}
void Database::lock()
{
common->lock(false);
}
void Database::lockAllDatabases(CommonMap &commons, bool forSleep)
{
StLock<Mutex> _(commons); for (CommonMap::iterator it = commons.begin(); it != commons.end(); it++)
it->second->lock(true, forSleep); }
KeyBlob *Database::encodeKey(const CssmKey &key, const CssmData &pubAcl, const CssmData &privAcl)
{
unlock();
return common->encodeKeyCore(key, pubAcl, privAcl);
}
void Database::decodeKey(KeyBlob *blob, CssmKey &key, void * &pubAcl, void * &privAcl)
{
unlock();
common->decodeKeyCore(blob, key, pubAcl, privAcl);
activity();
}
void Database::setParameters(const DBParameters ¶ms)
{
StLock<Mutex> _(*common);
makeUnlocked();
common->mParams = params;
common->version++; activity();
secdebug("SSdb", "%p common %p(%s) set params=(%ld,%d)",
this, common, dbName(), params.idleTimeout, params.lockOnSleep);
}
void Database::getParameters(DBParameters ¶ms)
{
StLock<Mutex> _(*common);
makeUnlocked();
params = common->mParams;
}
void Database::instantiateAcl()
{
StLock<Mutex> _(*common);
makeUnlocked();
}
void Database::changedAcl()
{
StLock<Mutex> _(*common);
version = 0;
}
const Database *Database::relatedDatabase() const
{ return this; }
#if defined(DEBUGDUMP)
void Database::debugDump(const char *msg)
{
assert(common);
const Signature &sig = common->identifier();
uint32 sig4; memcpy(&sig4, sig.bytes, sizeof(sig4));
Debug::dump("** %s(%8.8lx) common=%p(%ld) %s\n",
common->dbName(), sig4, common, common->useCount, msg);
if (isLocked())
Debug::dump(" locked");
else {
Time::Absolute when = common->when();
time_t whenTime = time_t(when);
Debug::dump(" UNLOCKED(%24.24s/%.2g)", ctime(&whenTime),
(when - Time::now()).seconds());
}
Debug::dump(" %s blobversion=%ld/%ld %svalidData",
(common->isValid() ? "validkeys" : "!validkeys"),
version, common->version,
(mValidData ? "" : "!"));
Debug::dump(" Params=(%ld %d)\n",
common->mParams.idleTimeout, common->mParams.lockOnSleep);
}
#endif //DEBUGDUMP
Database::Common::Common(const DbIdentifier &id, CommonMap &commonPool)
: pool(commonPool), mIdentifier(id), sequence(0), useCount(0), version(1),
mIsLocked(true), mValidParams(false)
{ }
Database::Common::~Common()
{
Server::active().clearTimer(this);
pool.erase(identifier());
}
void Database::Common::makeNewSecrets()
{
assert(hasMaster());
DatabaseCryptoCore::generateNewSecrets();
mIsLocked = false;
activity();
}
void Database::discard(Common *common)
{
secdebug("SSdb", "discarding dbcommon %p (no users, locked)", common);
delete common;
}
bool Database::Common::unlock(DbBlob *blob, void **privateAclBlob)
{
try {
assert(hasMaster());
decodeCore(blob, privateAclBlob);
secdebug("SSdb", "%p unlock successful", this);
} catch (...) {
secdebug("SSdb", "%p unlock failed", this);
return false;
}
if (!mValidParams) {
mParams = blob->params;
n2hi(mParams.idleTimeout);
mValidParams = true; }
mIsLocked = false;
activity();
KeychainNotifier::unlock(identifier());
return true;
}
void Database::Common::lock(bool holdingCommonLock, bool forSleep)
{
StLock<Mutex> locker(*this);
if (!isLocked()) {
if (forSleep && !mParams.lockOnSleep)
return;
mIsLocked = true;
DatabaseCryptoCore::invalidate();
KeychainNotifier::lock(identifier());
Server::active().clearTimer(this);
StLock<Mutex> _(pool, false);
if (!holdingCommonLock)
_.lock();
if (useCount == 0) {
locker.unlock(); discard(this);
}
}
}
DbBlob *Database::Common::encode(Database &db)
{
assert(!isLocked());
CssmData pubAcl, privAcl;
db.exportBlob(pubAcl, privAcl);
DbBlob form;
form.randomSignature = identifier();
form.sequence = sequence;
form.params = mParams;
h2ni(form.params.idleTimeout);
assert(hasMaster());
DbBlob *blob = encodeCore(form, pubAcl, privAcl);
db.allocator.free(pubAcl);
db.allocator.free(privAcl);
return blob;
}
void Database::Common::action()
{
secdebug("SSdb", "common %s(%p) locked by timer (%d refs)",
dbName(), this, int(useCount));
lock(false);
}
void Database::Common::activity()
{
if (!isLocked())
Server::active().setTimer(this, Time::Interval(int(mParams.idleTimeout)));
}
SystemKeychainKey::SystemKeychainKey(const char *path)
: mPath(path)
{
CssmKey::Header &hdr = mKey.header();
hdr.blobType(CSSM_KEYBLOB_RAW);
hdr.blobFormat(CSSM_KEYBLOB_RAW_FORMAT_OCTET_STRING);
hdr.keyClass(CSSM_KEYCLASS_SESSION_KEY);
hdr.algorithm(CSSM_ALGID_3DES_3KEY_EDE);
hdr.KeyAttr = 0;
hdr.KeyUsage = CSSM_KEYUSE_ANY;
mKey = CssmData::wrap(mBlob.masterKey);
}
SystemKeychainKey::~SystemKeychainKey()
{
}
bool SystemKeychainKey::matches(const DbBlob::Signature &signature)
{
return update() && signature == mBlob.signature;
}
bool SystemKeychainKey::update()
{
if (mUpdateThreshold > Time::now())
return mValid;
struct stat st;
if (::stat(mPath.c_str(), &st)) {
mUpdateThreshold = Time::now() + Time::Interval(checkDelay);
return mValid = false;
}
if (mValid && Time::Absolute(st.st_mtimespec) == mCachedDate)
return true;
mUpdateThreshold = Time::now() + Time::Interval(checkDelay);
try {
secdebug("syskc", "reading system unlock record from %s", mPath.c_str());
AutoFileDesc fd(mPath, O_RDONLY);
if (fd.read(mBlob) != sizeof(mBlob))
return false;
if (mBlob.isValid()) {
mCachedDate = st.st_mtimespec;
return mValid = true;
} else
return mValid = false;
} catch (...) {
secdebug("syskc", "system unlock record not available");
return false;
}
}