#include #include #include #include #include #include #include "mpw-cli-util.h" #include "mpw-algorithm.h" #include "mpw-util.h" #include "mpw-marshall.h" /** Output the program's usage documentation. */ static void usage() { inf( "" " Master Password v%s\n" "--------------------------------------------------------------------------------\n" " https://masterpasswordapp.com\n\n", stringify_def( MP_VERSION ) ); inf( "" "\nUSAGE\n\n" " mpw [-u|-U full-name] [-m fd] [-t pw-type] [-P value] [-c counter]\n" " [-a version] [-p purpose] [-C context] [-f|-F format] [-R 0|1]\n" " [-v|-q] [-h] site-name\n\n" ); inf( "" " -u full-name Specify the full name of the user.\n" " -u checks the master password against the config,\n" " -U allows updating to a new master password.\n" " Defaults to %s in env or prompts.\n\n", MP_ENV_fullName ); dbg( "" " -M master-pw Specify the master password of the user.\n" " Passing secrets as arguments is unsafe, for use in testing only.\n" ); inf( "" " -m fd Read the master password of the user from a file descriptor.\n" " Tip: don't send extra characters like newlines such as by using\n" " echo in a pipe. Consider printf instead.\n\n" ); inf( "" " -t pw-type Specify the password's template.\n" " Defaults to 'long' (-p a), 'name' (-p i) or 'phrase' (-p r).\n" " x, maximum | 20 characters, contains symbols.\n" " l, long | Copy-friendly, 14 characters, symbols.\n" " m, medium | Copy-friendly, 8 characters, symbols.\n" " b, basic | 8 characters, no symbols.\n" " s, short | Copy-friendly, 4 characters, no symbols.\n" " i, pin | 4 numbers.\n" " n, name | 9 letter name.\n" " p, phrase | 20 character sentence.\n" " K, key | encryption key (set key size -s bits).\n" " P, personal | saved personal password (save with -s pw).\n\n" ); inf( "" " -P value The parameter value.\n" " -p i | The login name for the site.\n" " -t K | The bit size of the key to generate (eg. 256).\n" " -t P | The personal password to encrypt.\n\n" ); inf( "" " -c counter The value of the counter.\n" " Defaults to 1.\n\n" ); inf( "" " -a version The algorithm version to use, %d - %d.\n" " Defaults to %s in env or %d.\n\n", MPAlgorithmVersionFirst, MPAlgorithmVersionLast, MP_ENV_algorithm, MPAlgorithmVersionCurrent ); inf( "" " -p purpose The purpose of the generated token.\n" " Defaults to 'auth'.\n" " a, auth | An authentication token such as a password.\n" " i, ident | An identification token such as a username.\n" " r, rec | A recovery token such as a security answer.\n\n" ); inf( "" " -C context A purpose-specific context.\n" " Defaults to empty.\n" " -p a | -\n" " -p i | -\n" " -p r | Most significant word in security question.\n\n" ); inf( "" " -f|F format The mpsites format to use for reading/writing site parameters.\n" " -F forces the use of the given format,\n" " -f allows fallback/migration.\n" " Defaults to %s in env or json, falls back to plain.\n" " n, none | No file\n" " f, flat | ~/.mpw.d/Full Name.%s\n" " j, json | ~/.mpw.d/Full Name.%s\n\n", MP_ENV_format, mpw_marshall_format_extension( MPMarshallFormatFlat ), mpw_marshall_format_extension( MPMarshallFormatJSON ) ); inf( "" " -R redacted Whether to save the mpsites in redacted format or not.\n" " Defaults to 1, redacted.\n\n" ); inf( "" " -v Increase output verbosity (can be repeated).\n" " -q Decrease output verbosity (can be repeated).\n\n" ); inf( "" "\nENVIRONMENT\n\n" " %-12s The full name of the user (see -u).\n" " %-12s The default algorithm version (see -a).\n" " %-12s The default mpsites format (see -f).\n" " %-12s The askpass program to use for prompting the user.\n\n", MP_ENV_fullName, MP_ENV_algorithm, MP_ENV_format, MP_ENV_askpass ); exit( 0 ); } /** ======================================================================== * MAIN */ int main(const int argc, char *const argv[]) { // CLI defaults. bool allowPasswordUpdate = false, sitesFormatFixed = false; // Read the environment. const char *fullNameArg = NULL, *masterPasswordFDArg = NULL, *masterPasswordArg = NULL, *siteNameArg = NULL; const char *resultTypeArg = NULL, *resultParamArg = NULL, *siteCounterArg = NULL, *algorithmVersionArg = NULL; const char *keyPurposeArg = NULL, *keyContextArg = NULL, *sitesFormatArg = NULL, *sitesRedactedArg = NULL; fullNameArg = mpw_getenv( MP_ENV_fullName ); algorithmVersionArg = mpw_getenv( MP_ENV_algorithm ); sitesFormatArg = mpw_getenv( MP_ENV_format ); // Read the command-line options. for (int opt; (opt = getopt( argc, argv, "u:U:m:M:t:P:c:a:p:C:f:F:R:vqh" )) != EOF;) switch (opt) { case 'u': fullNameArg = optarg && strlen( optarg )? strdup( optarg ): NULL; allowPasswordUpdate = false; break; case 'U': fullNameArg = optarg && strlen( optarg )? strdup( optarg ): NULL; allowPasswordUpdate = true; break; case 'm': masterPasswordFDArg = optarg && strlen( optarg )? strdup( optarg ): NULL; break; case 'M': // Passing your master password via the command-line is insecure. Testing purposes only. masterPasswordArg = optarg && strlen( optarg )? strdup( optarg ): NULL; break; case 't': resultTypeArg = optarg && strlen( optarg )? strdup( optarg ): NULL; break; case 'P': resultParamArg = optarg && strlen( optarg )? strdup( optarg ): NULL; break; case 'c': siteCounterArg = optarg && strlen( optarg )? strdup( optarg ): NULL; break; case 'a': algorithmVersionArg = optarg && strlen( optarg )? strdup( optarg ): NULL; break; case 'p': keyPurposeArg = optarg && strlen( optarg )? strdup( optarg ): NULL; break; case 'C': keyContextArg = optarg && strlen( optarg )? strdup( optarg ): NULL; break; case 'f': sitesFormatArg = optarg && strlen( optarg )? strdup( optarg ): NULL; sitesFormatFixed = false; break; case 'F': sitesFormatArg = optarg && strlen( optarg )? strdup( optarg ): NULL; sitesFormatFixed = true; break; case 'R': sitesRedactedArg = optarg && strlen( optarg )? strdup( optarg ): NULL; break; case 'v': ++mpw_verbosity; break; case 'q': --mpw_verbosity; break; case 'h': usage(); break; case '?': switch (optopt) { case 'u': ftl( "Missing full name to option: -%c\n", optopt ); return EX_USAGE; case 't': ftl( "Missing type name to option: -%c\n", optopt ); return EX_USAGE; case 'c': ftl( "Missing counter value to option: -%c\n", optopt ); return EX_USAGE; default: ftl( "Unknown option: -%c\n", optopt ); return EX_USAGE; } default: ftl( "Unexpected option: %c\n", opt ); return EX_USAGE; } if (optind < argc && argv[optind]) siteNameArg = strdup( argv[optind] ); // Determine fullName, siteName & masterPassword. const char *fullName = NULL, *masterPassword = NULL, *siteName = NULL; if ((!fullName || !strlen( fullName )) && fullNameArg) fullName = strdup( fullNameArg ); if (!fullName || !strlen( fullName )) do { fullName = mpw_getline( "Your full name:" ); } while (fullName && !strlen( fullName )); if (!fullName || !strlen( fullName )) { ftl( "Missing full name.\n" ); mpw_free_strings( &fullName, &masterPassword, &siteName, NULL ); return EX_DATAERR; } if ((!masterPassword || !strlen( masterPassword )) && masterPasswordFDArg) { masterPassword = mpw_read_fd( atoi( masterPasswordFDArg ) ); if (!masterPassword && errno) wrn( "Error reading master password from FD %s: %s\n", masterPasswordFDArg, strerror( errno ) ); } if ((!masterPassword || !strlen( masterPassword )) && masterPasswordArg) masterPassword = strdup( masterPasswordArg ); if (!masterPassword || !strlen( masterPassword )) do { masterPassword = mpw_getpass( "Your master password: " ); } while (masterPassword && !strlen( masterPassword )); if (!masterPassword || !strlen( masterPassword )) { ftl( "Missing master password.\n" ); mpw_free_strings( &fullName, &masterPassword, &siteName, NULL ); return EX_DATAERR; } if ((!siteName || !strlen( siteName )) && siteNameArg) siteName = strdup( siteNameArg ); if (!siteName || !strlen( siteName )) do { siteName = mpw_getline( "Site name:" ); } while (siteName && !strlen( siteName )); if (!siteName || !strlen( siteName )) { ftl( "Missing site name.\n" ); mpw_free_strings( &fullName, &masterPassword, &siteName, NULL ); return EX_DATAERR; } MPMarshallFormat sitesFormat = MPMarshallFormatDefault; if (sitesFormatArg) { sitesFormat = mpw_formatWithName( sitesFormatArg ); if (ERR == (int)sitesFormat) { ftl( "Invalid sites format: %s\n", sitesFormatArg ); mpw_free_strings( &fullName, &masterPassword, &siteName, NULL ); return EX_USAGE; } } MPKeyPurpose keyPurpose = MPKeyPurposeAuthentication; if (keyPurposeArg) { keyPurpose = mpw_purposeWithName( keyPurposeArg ); if (ERR == (int)keyPurpose) { ftl( "Invalid purpose: %s\n", keyPurposeArg ); return EX_USAGE; } } const char *keyContext = NULL; if (keyContextArg) keyContext = strdup( keyContextArg ); // Find the user's sites file. FILE *sitesFile = NULL; const char *sitesPath = mpw_path( fullName, mpw_marshall_format_extension( sitesFormat ) ); if (!sitesPath || !(sitesFile = fopen( sitesPath, "r" ))) { dbg( "Couldn't open configuration file:\n %s: %s\n", sitesPath, strerror( errno ) ); // Try to fall back to the flat format. if (!sitesFormatFixed) { mpw_free_string( &sitesPath ); sitesPath = mpw_path( fullName, mpw_marshall_format_extension( MPMarshallFormatFlat ) ); if (sitesPath && (sitesFile = fopen( sitesPath, "r" ))) sitesFormat = MPMarshallFormatFlat; else dbg( "Couldn't open configuration file:\n %s: %s\n", sitesPath, strerror( errno ) ); } } // Load the user object from file. MPMarshalledUser *user = NULL; MPMarshalledSite *site = NULL; MPMarshalledQuestion *question = NULL; if (!sitesFile) mpw_free_string( &sitesPath ); else { // Read file. char *sitesInputData = mpw_read_file( sitesFile ); if (ferror( sitesFile )) wrn( "Error while reading configuration file:\n %s: %d\n", sitesPath, ferror( sitesFile ) ); fclose( sitesFile ); // Parse file. MPMarshallInfo *sitesInputInfo = mpw_marshall_read_info( sitesInputData ); MPMarshallFormat sitesInputFormat = sitesFormatArg? sitesFormat: sitesInputInfo->format; MPMarshallError marshallError = { .type = MPMarshallSuccess }; mpw_marshal_info_free( &sitesInputInfo ); user = mpw_marshall_read( sitesInputData, sitesInputFormat, masterPassword, &marshallError ); if (marshallError.type == MPMarshallErrorMasterPassword) { // Incorrect master password. if (!allowPasswordUpdate) { ftl( "Incorrect master password according to configuration:\n %s: %s\n", sitesPath, marshallError.description ); mpw_marshal_free( &user ); mpw_free_strings( &sitesInputData, &sitesPath, &fullName, &masterPassword, &siteName, NULL ); return EX_DATAERR; } // Update user's master password. while (marshallError.type == MPMarshallErrorMasterPassword) { inf( "Given master password does not match configuration.\n" ); inf( "To update the configuration with this new master password, first confirm the old master password.\n" ); const char *importMasterPassword = NULL; while (!importMasterPassword || !strlen( importMasterPassword )) importMasterPassword = mpw_getpass( "Old master password: " ); mpw_marshal_free( &user ); user = mpw_marshall_read( sitesInputData, sitesInputFormat, importMasterPassword, &marshallError ); mpw_free_string( &importMasterPassword ); } if (user) { mpw_free_string( &user->masterPassword ); user->masterPassword = strdup( masterPassword ); } } mpw_free_string( &sitesInputData ); if (!user || marshallError.type != MPMarshallSuccess) { err( "Couldn't parse configuration file:\n %s: %s\n", sitesPath, marshallError.description ); mpw_marshal_free( &user ); mpw_free_string( &sitesPath ); } } if (!user) user = mpw_marshall_user( fullName, masterPassword, MPAlgorithmVersionCurrent ); mpw_free_strings( &fullName, &masterPassword, NULL ); // Load the site object. for (size_t s = 0; s < user->sites_count; ++s) { site = &user->sites[s]; if (strcmp( siteName, site->name ) == 0) // Found. break; site = NULL; } if (!site) site = mpw_marshall_site( user, siteName, MPResultTypeDefault, MPCounterValueDefault, user->algorithm ); mpw_free_string( &siteName ); // Load the question object. switch (keyPurpose) { case MPKeyPurposeAuthentication: case MPKeyPurposeIdentification: break; case MPKeyPurposeRecovery: for (size_t q = 0; q < site->questions_count; ++q) { question = &site->questions[q]; if ((!keyContext && !strlen( question->keyword )) || (keyContext && strcmp( keyContext, question->keyword ) != 0)) // Found. break; question = NULL; } if (!question) question = mpw_marshal_question( site, keyContext ); break; } // Initialize purpose-specific operation parameters. MPResultType resultType = MPResultTypeDefault; MPCounterValue siteCounter = MPCounterValueDefault; const char *purposeResult = NULL, *resultState = NULL; switch (keyPurpose) { case MPKeyPurposeAuthentication: { purposeResult = "password"; resultType = site->type; resultState = site->content? strdup( site->content ): NULL; siteCounter = site->counter; break; } case MPKeyPurposeIdentification: { purposeResult = "login"; resultType = site->loginType; resultState = site->loginContent? strdup( site->loginContent ): NULL; siteCounter = MPCounterValueInitial; break; } case MPKeyPurposeRecovery: { mpw_free_string( &keyContext ); purposeResult = "answer"; keyContext = question->keyword? strdup( question->keyword ): NULL; resultType = question->type; resultState = question->content? strdup( question->content ): NULL; siteCounter = MPCounterValueInitial; break; } } // Override operation parameters from command-line arguments. if (resultTypeArg) { resultType = mpw_typeWithName( resultTypeArg ); if (ERR == (int)resultType) { ftl( "Invalid type: %s\n", resultTypeArg ); mpw_marshal_free( &user ); mpw_free_strings( &sitesPath, &resultState, &keyContext, NULL ); return EX_USAGE; } if (!(resultType & MPSiteFeatureAlternative)) { switch (keyPurpose) { case MPKeyPurposeAuthentication: site->type = resultType; break; case MPKeyPurposeIdentification: site->loginType = resultType; break; case MPKeyPurposeRecovery: question->type = resultType; break; } } } if (siteCounterArg) { long long int siteCounterInt = atoll( siteCounterArg ); if (siteCounterInt < MPCounterValueFirst || siteCounterInt > MPCounterValueLast) { ftl( "Invalid site counter: %s\n", siteCounterArg ); mpw_marshal_free( &user ); mpw_free_strings( &sitesPath, &resultState, &keyContext, NULL ); return EX_USAGE; } switch (keyPurpose) { case MPKeyPurposeAuthentication: siteCounter = site->counter = (MPCounterValue)siteCounterInt; break; case MPKeyPurposeIdentification: case MPKeyPurposeRecovery: // NOTE: counter for login & question is not persisted. break; } } const char *resultParam = NULL; if (resultParamArg) resultParam = strdup( resultParamArg ); if (algorithmVersionArg) { int algorithmVersionInt = atoi( algorithmVersionArg ); if (algorithmVersionInt < MPAlgorithmVersionFirst || algorithmVersionInt > MPAlgorithmVersionLast) { ftl( "Invalid algorithm version: %s\n", algorithmVersionArg ); mpw_marshal_free( &user ); mpw_free_strings( &sitesPath, &resultState, &keyContext, &resultParam, NULL ); return EX_USAGE; } site->algorithm = (MPAlgorithmVersion)algorithmVersionInt; } if (sitesRedactedArg) user->redacted = strcmp( sitesRedactedArg, "1" ) == 0; else if (!user->redacted) wrn( "Sites configuration is not redacted. Use -R 1 to change this.\n" ); mpw_free_strings( &fullNameArg, &masterPasswordArg, &siteNameArg, NULL ); mpw_free_strings( &resultTypeArg, &resultParamArg, &siteCounterArg, &algorithmVersionArg, NULL ); mpw_free_strings( &keyPurposeArg, &keyContextArg, &sitesFormatArg, &sitesRedactedArg, NULL ); // Operation summary. const char *identicon = mpw_identicon( user->fullName, user->masterPassword ); if (!identicon) wrn( "Couldn't determine identicon.\n" ); dbg( "-----------------\n" ); dbg( "fullName : %s\n", user->fullName ); trc( "masterPassword : %s\n", user->masterPassword ); dbg( "identicon : %s\n", identicon ); dbg( "sitesFormat : %s%s\n", mpw_nameForFormat( sitesFormat ), sitesFormatFixed? " (fixed)": "" ); dbg( "sitesPath : %s\n", sitesPath ); dbg( "siteName : %s\n", site->name ); dbg( "siteCounter : %u\n", siteCounter ); dbg( "resultType : %s (%u)\n", mpw_nameForType( resultType ), resultType ); dbg( "resultParam : %s\n", resultParam ); dbg( "keyPurpose : %s (%u)\n", mpw_nameForPurpose( keyPurpose ), keyPurpose ); dbg( "keyContext : %s\n", keyContext ); dbg( "algorithmVersion : %u\n", site->algorithm ); dbg( "-----------------\n\n" ); inf( "%s's %s for %s:\n[ %s ]: ", user->fullName, purposeResult, site->name, identicon ); mpw_free_strings( &identicon, &sitesPath, NULL ); // Determine master key. MPMasterKey masterKey = mpw_masterKey( user->fullName, user->masterPassword, site->algorithm ); if (!masterKey) { ftl( "Couldn't derive master key.\n" ); mpw_marshal_free( &user ); mpw_free_strings( &resultState, &keyContext, &resultParam, NULL ); return EX_SOFTWARE; } // Update state. if (resultParam && resultType & MPResultTypeClassStateful) { mpw_free_string( &resultState ); if (!(resultState = mpw_siteState( masterKey, site->name, siteCounter, keyPurpose, keyContext, resultType, resultParam, site->algorithm ))) { ftl( "Couldn't encrypt site result.\n" ); mpw_free( &masterKey, MPMasterKeySize ); mpw_marshal_free( &user ); mpw_free_strings( &resultState, &keyContext, &resultParam, NULL ); return EX_SOFTWARE; } inf( "(state) %s => ", resultState ); switch (keyPurpose) { case MPKeyPurposeAuthentication: { mpw_free_string( &site->content ); site->content = strdup( resultState ); break; } case MPKeyPurposeIdentification: { mpw_free_string( &site->loginContent ); site->loginContent = strdup( resultState ); break; } case MPKeyPurposeRecovery: { mpw_free_string( &question->content ); question->content = strdup( resultState ); break; } } // resultParam is consumed. mpw_free_string( &resultParam ); } // Second phase resultParam defaults to state. if (!resultParam && resultState) resultParam = strdup( resultState ); mpw_free_string( &resultState ); // Generate result. const char *result = mpw_siteResult( masterKey, site->name, siteCounter, keyPurpose, keyContext, resultType, resultParam, site->algorithm ); mpw_free( &masterKey, MPMasterKeySize ); mpw_free_strings( &keyContext, &resultParam, NULL ); if (!result) { ftl( "Couldn't generate site result.\n" ); mpw_marshal_free( &user ); return EX_SOFTWARE; } fprintf( stdout, "%s\n", result ); if (site->url) inf( "See: %s\n", site->url ); mpw_free_string( &result ); // Update usage metadata. site->lastUsed = user->lastUsed = time( NULL ); site->uses++; // Update the mpsites file. if (sitesFormat != MPMarshallFormatNone) { if (!sitesFormatFixed) sitesFormat = MPMarshallFormatDefault; sitesPath = mpw_path( user->fullName, mpw_marshall_format_extension( sitesFormat ) ); dbg( "Updating: %s (%s)\n", sitesPath, mpw_nameForFormat( sitesFormat ) ); if (!sitesPath || !mpw_mkdirs( sitesPath ) || !(sitesFile = fopen( sitesPath, "w" ))) wrn( "Couldn't create updated configuration file:\n %s: %s\n", sitesPath, strerror( errno ) ); else { char *buf = NULL; MPMarshallError marshallError = { .type = MPMarshallSuccess }; if (!mpw_marshall_write( &buf, sitesFormat, user, &marshallError ) || marshallError.type != MPMarshallSuccess) wrn( "Couldn't encode updated configuration file:\n %s: %s\n", sitesPath, marshallError.description ); else if (fwrite( buf, sizeof( char ), strlen( buf ), sitesFile ) != strlen( buf )) wrn( "Error while writing updated configuration file:\n %s: %d\n", sitesPath, ferror( sitesFile ) ); mpw_free_string( &buf ); fclose( sitesFile ); } mpw_free_string( &sitesPath ); } mpw_marshal_free( &user ); return 0; }