2
0

Log fixes, test improvements and some refactoring.

This commit is contained in:
Maarten Billemont 2017-09-23 19:11:06 -04:00
parent 888338e107
commit cecaf1b5cc
16 changed files with 201 additions and 147 deletions

View File

@ -5,10 +5,10 @@ plugins {
description = 'Master Password Algorithm Implementation' description = 'Master Password Algorithm Implementation'
dependencies { dependencies {
compile (group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.6-p10') { compile (group: 'com.lyndir.lhunath.opal', name: 'opal-system', version: '1.6-p11') {
exclude( module: 'joda-time' ) exclude( module: 'joda-time' )
} }
compile group: 'com.lyndir.lhunath.opal', name: 'opal-crypto', version: '1.6-p10' compile group: 'com.lyndir.lhunath.opal', name: 'opal-crypto', version: '1.6-p11'
compile group: 'com.lambdaworks', name: 'scrypt', version: '1.4.0' compile group: 'com.lambdaworks', name: 'scrypt', version: '1.4.0'
compile group: 'org.jetbrains', name: 'annotations', version: '13.0' compile group: 'org.jetbrains', name: 'annotations', version: '13.0'

View File

@ -23,7 +23,7 @@
<dependency> <dependency>
<groupId>com.lyndir.lhunath.opal</groupId> <groupId>com.lyndir.lhunath.opal</groupId>
<artifactId>opal-system</artifactId> <artifactId>opal-system</artifactId>
<version>1.6-p9</version> <version>1.6-p11</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>joda-time</groupId> <groupId>joda-time</groupId>
@ -34,7 +34,7 @@
<dependency> <dependency>
<groupId>com.lyndir.lhunath.opal</groupId> <groupId>com.lyndir.lhunath.opal</groupId>
<artifactId>opal-crypto</artifactId> <artifactId>opal-crypto</artifactId>
<version>1.6-p9</version> <version>1.6-p11</version>
</dependency> </dependency>
<!-- EXTERNAL DEPENDENCIES --> <!-- EXTERNAL DEPENDENCIES -->

View File

@ -30,12 +30,12 @@ public interface MPAlgorithm extends Serializable {
MPMasterKey.Version getAlgorithmVersion(); MPMasterKey.Version getAlgorithmVersion();
byte[] deriveKey(String fullName, char[] masterPassword); byte[] masterKey(String fullName, char[] masterPassword);
byte[] siteKey(byte[] masterKey, String siteName, UnsignedInteger siteCounter, MPKeyPurpose keyPurpose, byte[] siteKey(byte[] masterKey, String siteName, UnsignedInteger siteCounter, MPKeyPurpose keyPurpose,
@Nullable String keyContext); @Nullable String keyContext);
String siteResult(byte[] masterKey, String siteName, UnsignedInteger siteCounter, MPKeyPurpose keyPurpose, String siteResult(byte[] masterKey, final byte[] siteKey, String siteName, UnsignedInteger siteCounter, MPKeyPurpose keyPurpose,
@Nullable String keyContext, MPResultType resultType, @Nullable String resultParam); @Nullable String keyContext, MPResultType resultType, @Nullable String resultParam);
String sitePasswordFromTemplate(byte[] masterKey, byte[] siteKey, MPResultType resultType, @Nullable String resultParam); String sitePasswordFromTemplate(byte[] masterKey, byte[] siteKey, MPResultType resultType, @Nullable String resultParam);
@ -44,6 +44,6 @@ public interface MPAlgorithm extends Serializable {
String sitePasswordFromDerive(byte[] masterKey, byte[] siteKey, MPResultType resultType, @Nullable String resultParam); String sitePasswordFromDerive(byte[] masterKey, byte[] siteKey, MPResultType resultType, @Nullable String resultParam);
String siteState(byte[] masterKey, String siteName, UnsignedInteger siteCounter, MPKeyPurpose keyPurpose, String siteState(byte[] masterKey, final byte[] siteKey, String siteName, UnsignedInteger siteCounter, MPKeyPurpose keyPurpose,
@Nullable String keyContext, MPResultType resultType, @Nullable String resultParam); @Nullable String keyContext, MPResultType resultType, @Nullable String resultParam);
} }

View File

@ -90,16 +90,10 @@ public class MPAlgorithmV0 implements MPAlgorithm {
} }
@Override @Override
public byte[] deriveKey(final String fullName, final char[] masterPassword) { public byte[] masterKey(final String fullName, final char[] masterPassword) {
Preconditions.checkArgument( masterPassword.length > 0 );
byte[] fullNameBytes = fullName.getBytes( mpw_charset ); byte[] fullNameBytes = fullName.getBytes( mpw_charset );
byte[] fullNameLengthBytes = bytesForInt( fullName.length() ); byte[] fullNameLengthBytes = bytesForInt( fullName.length() );
ByteBuffer mpBytesBuf = mpw_charset.encode( CharBuffer.wrap( masterPassword ) );
logger.trc( "-- mpw_masterKey (algorithm: %u)", getAlgorithmVersion().toInt() );
logger.trc( "fullName: %s", fullName );
logger.trc( "masterPassword.id: %s", (Object) idForBytes( mpBytesBuf.array() ) );
String keyScope = MPKeyPurpose.Authentication.getScope(); String keyScope = MPKeyPurpose.Authentication.getScope();
logger.trc( "keyScope: %s", keyScope ); logger.trc( "keyScope: %s", keyScope );
@ -111,15 +105,13 @@ public class MPAlgorithmV0 implements MPAlgorithm {
logger.trc( " => masterKeySalt.id: %s", CodeUtils.encodeHex( idForBytes( masterKeySalt ) ) ); logger.trc( " => masterKeySalt.id: %s", CodeUtils.encodeHex( idForBytes( masterKeySalt ) ) );
// Calculate the master key. // Calculate the master key.
logger.trc( "masterKey: scrypt( masterPassword, masterKeySalt, N=%lu, r=%u, p=%u )", logger.trc( "masterKey: scrypt( masterPassword, masterKeySalt, N=%d, r=%d, p=%d )",
scrypt_N, scrypt_r, scrypt_p ); scrypt_N, scrypt_r, scrypt_p );
byte[] mpBytes = new byte[mpBytesBuf.remaining()]; byte[] masterPasswordBytes = bytesForChars( masterPassword );
mpBytesBuf.get( mpBytes, 0, mpBytes.length ); byte[] masterKey = scrypt( masterKeySalt, masterPasswordBytes );
Arrays.fill( mpBytesBuf.array(), (byte) 0 );
byte[] masterKey = scrypt( masterKeySalt, mpBytes ); // TODO: Why not mpBytesBuf.array()?
Arrays.fill( masterKeySalt, (byte) 0 ); Arrays.fill( masterKeySalt, (byte) 0 );
Arrays.fill( mpBytes, (byte) 0 ); Arrays.fill( masterPasswordBytes, (byte) 0 );
logger.trc( " => masterKey.id: %s", (Object) idForBytes( masterKey ) ); logger.trc( " => masterKey.id: %s", CodeUtils.encodeHex( idForBytes( masterKey ) ) );
return masterKey; return masterKey;
} }
@ -139,13 +131,6 @@ public class MPAlgorithmV0 implements MPAlgorithm {
@Override @Override
public byte[] siteKey(final byte[] masterKey, final String siteName, UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose, public byte[] siteKey(final byte[] masterKey, final String siteName, UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose,
@Nullable final String keyContext) { @Nullable final String keyContext) {
Preconditions.checkArgument( !siteName.isEmpty() );
logger.trc( "-- mpw_siteKey (algorithm: %u)", getAlgorithmVersion().toInt() );
logger.trc( "siteName: %s", siteName );
logger.trc( "siteCounter: %d", siteCounter );
logger.trc( "keyPurpose: %d (%s)", keyPurpose.toInt(), keyPurpose.getShortName() );
logger.trc( "keyContext: %s", keyContext );
String keyScope = keyPurpose.getScope(); String keyScope = keyPurpose.getScope();
logger.trc( "keyScope: %s", keyScope ); logger.trc( "keyScope: %s", keyScope );
@ -169,23 +154,18 @@ public class MPAlgorithmV0 implements MPAlgorithm {
sitePasswordInfo = Bytes.concat( sitePasswordInfo, keyContextLengthBytes, keyContextBytes ); sitePasswordInfo = Bytes.concat( sitePasswordInfo, keyContextLengthBytes, keyContextBytes );
logger.trc( " => siteSalt.id: %s", CodeUtils.encodeHex( idForBytes( sitePasswordInfo ) ) ); logger.trc( " => siteSalt.id: %s", CodeUtils.encodeHex( idForBytes( sitePasswordInfo ) ) );
logger.trc( "siteKey: hmac-sha256( masterKey.id=%s, siteSalt )", (Object) idForBytes( masterKey ) ); logger.trc( "siteKey: hmac-sha256( masterKey.id=%s, siteSalt )", CodeUtils.encodeHex( idForBytes( masterKey ) ) );
byte[] sitePasswordSeedBytes = mpw_digest.of( masterKey, sitePasswordInfo ); byte[] sitePasswordSeedBytes = mpw_digest.of( masterKey, sitePasswordInfo );
logger.trc( " => siteKey.id: %s", (Object) idForBytes( sitePasswordSeedBytes ) ); logger.trc( " => siteKey.id: %s", CodeUtils.encodeHex( idForBytes( sitePasswordSeedBytes ) ) );
return sitePasswordSeedBytes; return sitePasswordSeedBytes;
} }
@Override @Override
public String siteResult(final byte[] masterKey, final String siteName, final UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose, public String siteResult(final byte[] masterKey, final byte[] siteKey, final String siteName, final UnsignedInteger siteCounter,
final MPKeyPurpose keyPurpose,
@Nullable final String keyContext, final MPResultType resultType, @Nullable final String resultParam) { @Nullable final String keyContext, final MPResultType resultType, @Nullable final String resultParam) {
byte[] siteKey = siteKey( masterKey, siteName, siteCounter, keyPurpose, keyContext );
logger.trc( "-- mpw_siteResult (algorithm: %u)", getAlgorithmVersion().toInt() );
logger.trc( "resultType: %d (%s)", resultType.toInt(), resultType.getShortName() );
logger.trc( "resultParam: %s", resultParam );
switch (resultType.getTypeClass()) { switch (resultType.getTypeClass()) {
case Template: case Template:
return sitePasswordFromTemplate( masterKey, siteKey, resultType, resultParam ); return sitePasswordFromTemplate( masterKey, siteKey, resultType, resultParam );
@ -214,7 +194,7 @@ public class MPAlgorithmV0 implements MPAlgorithm {
Preconditions.checkState( _siteKey.length > 0 ); Preconditions.checkState( _siteKey.length > 0 );
int templateIndex = _siteKey[0]; int templateIndex = _siteKey[0];
MPTemplate template = resultType.getTemplateAtRollingIndex( templateIndex ); MPTemplate template = resultType.getTemplateAtRollingIndex( templateIndex );
logger.trc( "template: %u => %s", templateIndex, template.getTemplateString() ); logger.trc( "template: %d => %s", templateIndex, template.getTemplateString() );
// Encode the password from the seed using the template. // Encode the password from the seed using the template.
StringBuilder password = new StringBuilder( template.length() ); StringBuilder password = new StringBuilder( template.length() );
@ -222,7 +202,7 @@ public class MPAlgorithmV0 implements MPAlgorithm {
int characterIndex = _siteKey[i + 1]; int characterIndex = _siteKey[i + 1];
MPTemplateCharacterClass characterClass = template.getCharacterClassAtIndex( i ); MPTemplateCharacterClass characterClass = template.getCharacterClassAtIndex( i );
char passwordCharacter = characterClass.getCharacterAtRollingIndex( characterIndex ); char passwordCharacter = characterClass.getCharacterAtRollingIndex( characterIndex );
logger.trc( " - class: %c, index: %5u (0x%02hX) => character: %c", logger.trc( " - class: %c, index: %5d (0x%2H) => character: %c",
characterClass.getIdentifier(), characterIndex, _siteKey[i + 1], passwordCharacter ); characterClass.getIdentifier(), characterIndex, _siteKey[i + 1], passwordCharacter );
password.append( passwordCharacter ); password.append( passwordCharacter );
@ -241,7 +221,7 @@ public class MPAlgorithmV0 implements MPAlgorithm {
try { try {
// Base64-decode // Base64-decode
byte[] cipherBuf = CryptUtils.decodeBase64( resultParam ); byte[] cipherBuf = CryptUtils.decodeBase64( resultParam );
logger.trc( "b64 decoded: %zu bytes = %s", cipherBuf.length, CodeUtils.encodeHex( cipherBuf ) ); logger.trc( "b64 decoded: %d bytes = %s", cipherBuf.length, CodeUtils.encodeHex( cipherBuf ) );
// Decrypt // Decrypt
byte[] plainBuf = CryptUtils.decrypt( cipherBuf, masterKey, true ); byte[] plainBuf = CryptUtils.decrypt( cipherBuf, masterKey, true );
@ -266,7 +246,7 @@ public class MPAlgorithmV0 implements MPAlgorithm {
if ((resultParamInt < 128) || (resultParamInt > 512) || ((resultParamInt % 8) != 0)) if ((resultParamInt < 128) || (resultParamInt > 512) || ((resultParamInt % 8) != 0))
throw logger.bug( "Parameter is not a valid key size (should be 128 - 512): %s", resultParam ); throw logger.bug( "Parameter is not a valid key size (should be 128 - 512): %s", resultParam );
int keySize = resultParamInt / 8; int keySize = resultParamInt / 8;
logger.trc( "keySize: %u", keySize ); logger.trc( "keySize: %d", keySize );
// Derive key // Derive key
byte[] resultKey = null; // TODO: mpw_kdf_blake2b( keySize, siteKey, MPSiteKeySize, NULL, 0, 0, NULL ); byte[] resultKey = null; // TODO: mpw_kdf_blake2b( keySize, siteKey, MPSiteKeySize, NULL, 0, 0, NULL );
@ -285,7 +265,8 @@ public class MPAlgorithmV0 implements MPAlgorithm {
} }
@Override @Override
public String siteState(final byte[] masterKey, final String siteName, final UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose, public String siteState(final byte[] masterKey, final byte[] siteKey, final String siteName, final UnsignedInteger siteCounter,
final MPKeyPurpose keyPurpose,
@Nullable final String keyContext, final MPResultType resultType, @Nullable final String resultParam) { @Nullable final String keyContext, final MPResultType resultType, @Nullable final String resultParam) {
Preconditions.checkNotNull( resultParam ); Preconditions.checkNotNull( resultParam );
@ -293,9 +274,8 @@ public class MPAlgorithmV0 implements MPAlgorithm {
try { try {
// Encrypt // Encrypt
ByteBuffer plainText = mpw_charset.encode( CharBuffer.wrap( resultParam ) ); byte[] cipherBuf = CryptUtils.encrypt( resultParam.getBytes( mpw_charset ), masterKey, true );
byte[] cipherBuf = CryptUtils.encrypt( plainText.array(), masterKey, true ); logger.trc( "cipherBuf: %d bytes = %s", cipherBuf.length, CodeUtils.encodeHex( cipherBuf ) );
logger.trc( "cipherBuf: %zu bytes = %s", cipherBuf.length, CodeUtils.encodeHex( cipherBuf ) );
// Base64-encode // Base64-encode
String cipherText = Verify.verifyNotNull( CryptUtils.encodeBase64( cipherBuf ) ); String cipherText = Verify.verifyNotNull( CryptUtils.encodeBase64( cipherBuf ) );

View File

@ -38,15 +38,11 @@ public class MPAlgorithmV1 extends MPAlgorithmV0 {
@Override @Override
public String sitePasswordFromTemplate(final byte[] masterKey, final byte[] siteKey, final MPResultType resultType, @Nullable final String resultParam) { public String sitePasswordFromTemplate(final byte[] masterKey, final byte[] siteKey, final MPResultType resultType, @Nullable final String resultParam) {
logger.trc( "-- mpw_siteResult (algorithm: %u)", getAlgorithmVersion().toInt() );
logger.trc( "resultType: %d (%s)", resultType.toInt(), resultType.getShortName() );
logger.trc( "resultParam: %s", resultParam );
// Determine the template. // Determine the template.
Preconditions.checkState( siteKey.length > 0 ); Preconditions.checkState( siteKey.length > 0 );
int templateIndex = siteKey[0] & 0xFF; // Convert to unsigned int. int templateIndex = siteKey[0] & 0xFF; // Convert to unsigned int.
MPTemplate template = resultType.getTemplateAtRollingIndex( templateIndex ); MPTemplate template = resultType.getTemplateAtRollingIndex( templateIndex );
logger.trc( "template: %u => %s", templateIndex, template.getTemplateString() ); logger.trc( "template: %d => %s", templateIndex, template.getTemplateString() );
// Encode the password from the seed using the template. // Encode the password from the seed using the template.
StringBuilder password = new StringBuilder( template.length() ); StringBuilder password = new StringBuilder( template.length() );
@ -54,7 +50,7 @@ public class MPAlgorithmV1 extends MPAlgorithmV0 {
int characterIndex = siteKey[i + 1] & 0xFF; // Convert to unsigned int. int characterIndex = siteKey[i + 1] & 0xFF; // Convert to unsigned int.
MPTemplateCharacterClass characterClass = template.getCharacterClassAtIndex( i ); MPTemplateCharacterClass characterClass = template.getCharacterClassAtIndex( i );
char passwordCharacter = characterClass.getCharacterAtRollingIndex( characterIndex ); char passwordCharacter = characterClass.getCharacterAtRollingIndex( characterIndex );
logger.trc( " - class: %c, index: %3u (0x%02hhX) => character: %c", logger.trc( " - class: %c, index: %3d (0x%2H) => character: %c",
characterClass.getIdentifier(), characterIndex, siteKey[i + 1], passwordCharacter ); characterClass.getIdentifier(), characterIndex, siteKey[i + 1], passwordCharacter );
password.append( passwordCharacter ); password.append( passwordCharacter );

View File

@ -43,13 +43,6 @@ public class MPAlgorithmV2 extends MPAlgorithmV1 {
@Override @Override
public byte[] siteKey(final byte[] masterKey, final String siteName, UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose, public byte[] siteKey(final byte[] masterKey, final String siteName, UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose,
@Nullable final String keyContext) { @Nullable final String keyContext) {
Preconditions.checkArgument( !siteName.isEmpty() );
logger.trc( "-- mpw_siteKey (algorithm: %u)", getAlgorithmVersion().toInt() );
logger.trc( "siteName: %s", siteName );
logger.trc( "siteCounter: %d", siteCounter );
logger.trc( "keyPurpose: %d (%s)", keyPurpose.toInt(), keyPurpose.getShortName() );
logger.trc( "keyContext: %s", keyContext );
String keyScope = keyPurpose.getScope(); String keyScope = keyPurpose.getScope();
logger.trc( "keyScope: %s", keyScope ); logger.trc( "keyScope: %s", keyScope );
@ -73,9 +66,9 @@ public class MPAlgorithmV2 extends MPAlgorithmV1 {
sitePasswordInfo = Bytes.concat( sitePasswordInfo, keyContextLengthBytes, keyContextBytes ); sitePasswordInfo = Bytes.concat( sitePasswordInfo, keyContextLengthBytes, keyContextBytes );
logger.trc( " => siteSalt.id: %s", CodeUtils.encodeHex( idForBytes( sitePasswordInfo ) ) ); logger.trc( " => siteSalt.id: %s", CodeUtils.encodeHex( idForBytes( sitePasswordInfo ) ) );
logger.trc( "siteKey: hmac-sha256( masterKey.id=%s, siteSalt )", (Object) idForBytes( masterKey ) ); logger.trc( "siteKey: hmac-sha256( masterKey.id=%s, siteSalt )", CodeUtils.encodeHex( idForBytes( masterKey ) ) );
byte[] sitePasswordSeedBytes = MPAlgorithmV0.mpw_digest.of( masterKey, sitePasswordInfo ); byte[] sitePasswordSeedBytes = MPAlgorithmV0.mpw_digest.of( masterKey, sitePasswordInfo );
logger.trc( " => siteKey.id: %s", (Object) idForBytes( sitePasswordSeedBytes ) ); logger.trc( " => siteKey.id: %s", CodeUtils.encodeHex( idForBytes( sitePasswordSeedBytes ) ) );
return sitePasswordSeedBytes; return sitePasswordSeedBytes;
} }

View File

@ -18,7 +18,7 @@
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import static com.lyndir.masterpassword.MPUtils.idForBytes; import static com.lyndir.masterpassword.MPUtils.*;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
@ -42,16 +42,10 @@ public class MPAlgorithmV3 extends MPAlgorithmV2 {
} }
@Override @Override
public byte[] deriveKey(final String fullName, final char[] masterPassword) { public byte[] masterKey(final String fullName, final char[] masterPassword) {
Preconditions.checkArgument( masterPassword.length > 0 );
byte[] fullNameBytes = fullName.getBytes( MPAlgorithmV0.mpw_charset ); byte[] fullNameBytes = fullName.getBytes( MPAlgorithmV0.mpw_charset );
byte[] fullNameLengthBytes = MPUtils.bytesForInt( fullNameBytes.length ); byte[] fullNameLengthBytes = MPUtils.bytesForInt( fullNameBytes.length );
ByteBuffer mpBytesBuf = MPAlgorithmV0.mpw_charset.encode( CharBuffer.wrap( masterPassword ) );
logger.trc( "-- mpw_masterKey (algorithm: %u)", getAlgorithmVersion().toInt() );
logger.trc( "fullName: %s", fullName );
logger.trc( "masterPassword.id: %s", (Object) idForBytes( mpBytesBuf.array() ) );
String keyScope = MPKeyPurpose.Authentication.getScope(); String keyScope = MPKeyPurpose.Authentication.getScope();
logger.trc( "keyScope: %s", keyScope ); logger.trc( "keyScope: %s", keyScope );
@ -63,15 +57,13 @@ public class MPAlgorithmV3 extends MPAlgorithmV2 {
logger.trc( " => masterKeySalt.id: %s", CodeUtils.encodeHex( idForBytes( masterKeySalt ) ) ); logger.trc( " => masterKeySalt.id: %s", CodeUtils.encodeHex( idForBytes( masterKeySalt ) ) );
// Calculate the master key. // Calculate the master key.
logger.trc( "masterKey: scrypt( masterPassword, masterKeySalt, N=%lu, r=%u, p=%u )", logger.trc( "masterKey: scrypt( masterPassword, masterKeySalt, N=%d, r=%d, p=%d )",
MPAlgorithmV0.scrypt_N, MPAlgorithmV0.scrypt_r, MPAlgorithmV0.scrypt_p ); MPAlgorithmV0.scrypt_N, MPAlgorithmV0.scrypt_r, MPAlgorithmV0.scrypt_p );
byte[] mpBytes = new byte[mpBytesBuf.remaining()]; byte[] mpBytes = bytesForChars( masterPassword );
mpBytesBuf.get( mpBytes, 0, mpBytes.length ); byte[] masterKey = scrypt( masterKeySalt, mpBytes );
Arrays.fill( mpBytesBuf.array(), (byte) 0 );
byte[] masterKey = scrypt( masterKeySalt, mpBytes ); // TODO: Why not mpBytesBuf.array()?
Arrays.fill( masterKeySalt, (byte) 0 ); Arrays.fill( masterKeySalt, (byte) 0 );
Arrays.fill( mpBytes, (byte) 0 ); Arrays.fill( mpBytes, (byte) 0 );
logger.trc( " => masterKey.id: %s", (Object) idForBytes( masterKey ) ); logger.trc( " => masterKey.id: %s", CodeUtils.encodeHex( idForBytes( masterKey ) ) );
return masterKey; return masterKey;
} }

View File

@ -18,9 +18,11 @@
package com.lyndir.masterpassword; package com.lyndir.masterpassword;
import static com.lyndir.masterpassword.MPUtils.idForBytes; import static com.lyndir.masterpassword.MPUtils.*;
import com.google.common.base.Preconditions;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import java.util.Arrays; import java.util.Arrays;
import java.util.EnumMap; import java.util.EnumMap;
@ -53,6 +55,51 @@ public class MPMasterKey {
this.masterPassword = masterPassword; this.masterPassword = masterPassword;
} }
/**
* Derive the master key for a user based on their name and master password.
*
* @throws MPInvalidatedException {@link #invalidate()} has been called on this object.
*/
private byte[] masterKey(final Version algorithmVersion)
throws MPInvalidatedException {
Preconditions.checkArgument( masterPassword.length > 0 );
if (invalidated)
throw new MPInvalidatedException();
byte[] key = keyByVersion.get( algorithmVersion );
if (key == null) {
logger.trc( "-- mpw_masterKey (algorithm: %d)", algorithmVersion.toInt() );
logger.trc( "fullName: %s", fullName );
logger.trc( "masterPassword.id: %s", CodeUtils.encodeHex( idForBytes( bytesForChars( masterPassword ) ) ) );
keyByVersion.put( algorithmVersion, key = algorithmVersion.getAlgorithm().masterKey( fullName, masterPassword ) );
}
return key;
}
/**
* Derive the master key for a user based on their name and master password.
*
* @throws MPInvalidatedException {@link #invalidate()} has been called on this object.
*/
private byte[] siteKey(final String siteName, final UnsignedInteger siteCounter, final MPKeyPurpose keyPurpose,
@Nullable final String keyContext, final Version algorithmVersion)
throws MPInvalidatedException {
Preconditions.checkArgument( !siteName.isEmpty() );
byte[] masterKey = masterKey( algorithmVersion );
logger.trc( "-- mpw_siteKey (algorithm: %d)", algorithmVersion.toInt() );
logger.trc( "siteName: %s", siteName );
logger.trc( "siteCounter: %s", siteCounter );
logger.trc( "keyPurpose: %d (%s)", keyPurpose.toInt(), keyPurpose.getShortName() );
logger.trc( "keyContext: %s", keyContext );
return algorithmVersion.getAlgorithm().siteKey( masterKey, siteName, siteCounter, keyPurpose, keyContext );
}
/** /**
* Generate a site result token. * Generate a site result token.
* *
@ -70,8 +117,16 @@ public class MPMasterKey {
@Nullable final String keyContext, final MPResultType resultType, @Nullable final String resultParam, @Nullable final String keyContext, final MPResultType resultType, @Nullable final String resultParam,
final Version algorithmVersion) final Version algorithmVersion)
throws MPInvalidatedException { throws MPInvalidatedException {
byte[] masterKey = masterKey( algorithmVersion );
byte[] siteKey = siteKey( siteName, siteCounter, keyPurpose, keyContext, algorithmVersion );
logger.trc( "-- mpw_siteResult (algorithm: %d)", algorithmVersion.toInt() );
logger.trc( "resultType: %d (%s)", resultType.getType(), resultType.getShortName() );
logger.trc( "resultParam: %s", resultParam );
return algorithmVersion.getAlgorithm().siteResult( return algorithmVersion.getAlgorithm().siteResult(
getKey( algorithmVersion ), siteName, siteCounter, keyPurpose, keyContext, resultType, resultParam ); masterKey, siteKey, siteName, siteCounter, keyPurpose, keyContext, resultType, resultParam );
} }
/** /**
@ -91,8 +146,19 @@ public class MPMasterKey {
@Nullable final String keyContext, final MPResultType resultType, @Nullable final String resultParam, @Nullable final String keyContext, final MPResultType resultType, @Nullable final String resultParam,
final Version algorithmVersion) final Version algorithmVersion)
throws MPInvalidatedException { throws MPInvalidatedException {
Preconditions.checkNotNull( resultParam );
Preconditions.checkArgument( !resultParam.isEmpty() );
byte[] masterKey = masterKey( algorithmVersion );
byte[] siteKey = siteKey( siteName, siteCounter, keyPurpose, keyContext, algorithmVersion );
logger.trc( "-- mpw_siteState (algorithm: %d)", algorithmVersion.toInt() );
logger.trc( "resultType: %d (%s)", resultType.getType(), resultType.getShortName() );
logger.trc( "resultParam: %s", resultParam );
return algorithmVersion.getAlgorithm().siteState( return algorithmVersion.getAlgorithm().siteState(
getKey( algorithmVersion ), siteName, siteCounter, keyPurpose, keyContext, resultType, resultParam ); masterKey, siteKey, siteName, siteCounter, keyPurpose, keyContext, resultType, resultParam );
} }
@Nonnull @Nonnull
@ -109,7 +175,7 @@ public class MPMasterKey {
public byte[] getKeyID(final Version algorithmVersion) public byte[] getKeyID(final Version algorithmVersion)
throws MPInvalidatedException { throws MPInvalidatedException {
return idForBytes( getKey( algorithmVersion ) ); return idForBytes( masterKey( algorithmVersion ) );
} }
/** /**
@ -123,18 +189,6 @@ public class MPMasterKey {
Arrays.fill( masterPassword, (char) 0 ); Arrays.fill( masterPassword, (char) 0 );
} }
private byte[] getKey(final Version algorithmVersion)
throws MPInvalidatedException {
if (invalidated)
throw new MPInvalidatedException();
byte[] key = keyByVersion.get( algorithmVersion );
if (key == null)
keyByVersion.put( algorithmVersion, key = algorithmVersion.getAlgorithm().deriveKey( fullName, masterPassword ) );
return key;
}
/** /**
* The algorithm iterations. * The algorithm iterations.
*/ */

View File

@ -37,7 +37,7 @@ public enum MPResultType {
/** /**
* pg^VMAUBk5x3p%HP%i4= * pg^VMAUBk5x3p%HP%i4=
*/ */
GeneratedMaximum( "Maximum", "20 characters, contains symbols.", // GeneratedMaximum( "maximum", "20 characters, contains symbols.", //
ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ), ImmutableList.of( new MPTemplate( "anoxxxxxxxxxxxxxxxxx" ),
new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), // new MPTemplate( "axxxxxxxxxxxxxxxxxno" ) ), //
MPResultTypeClass.Template, 0x0 ), MPResultTypeClass.Template, 0x0 ),
@ -45,7 +45,7 @@ public enum MPResultType {
/** /**
* BiroYena8:Kixa * BiroYena8:Kixa
*/ */
GeneratedLong( "Long", "Copy-friendly, 14 characters, contains symbols.", // GeneratedLong( "long", "Copy-friendly, 14 characters, contains symbols.", //
ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ), ImmutableList.of( new MPTemplate( "CvcvnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvnoCvcv" ),
new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ), new MPTemplate( "CvcvCvcvCvcvno" ), new MPTemplate( "CvccnoCvcvCvcv" ),
new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ), new MPTemplate( "CvccCvcvnoCvcv" ), new MPTemplate( "CvccCvcvCvcvno" ),
@ -62,7 +62,7 @@ public enum MPResultType {
/** /**
* BirSuj0- * BirSuj0-
*/ */
GeneratedMedium( "Medium", "Copy-friendly, 8 characters, contains symbols.", // GeneratedMedium( "medium", "Copy-friendly, 8 characters, contains symbols.", //
ImmutableList.of( new MPTemplate( "CvcnoCvc" ), ImmutableList.of( new MPTemplate( "CvcnoCvc" ),
new MPTemplate( "CvcCvcno" ) ), // new MPTemplate( "CvcCvcno" ) ), //
MPResultTypeClass.Template, 0x2 ), MPResultTypeClass.Template, 0x2 ),
@ -70,7 +70,7 @@ public enum MPResultType {
/** /**
* pO98MoD0 * pO98MoD0
*/ */
GeneratedBasic( "Basic", "8 characters, no symbols.", // GeneratedBasic( "basic", "8 characters, no symbols.", //
ImmutableList.of( new MPTemplate( "aaanaaan" ), ImmutableList.of( new MPTemplate( "aaanaaan" ),
new MPTemplate( "aannaaan" ), new MPTemplate( "aannaaan" ),
new MPTemplate( "aaannaaa" ) ), // new MPTemplate( "aaannaaa" ) ), //
@ -79,28 +79,28 @@ public enum MPResultType {
/** /**
* Bir8 * Bir8
*/ */
GeneratedShort( "Short", "Copy-friendly, 4 characters, no symbols.", // GeneratedShort( "short", "Copy-friendly, 4 characters, no symbols.", //
ImmutableList.of( new MPTemplate( "Cvcn" ) ), // ImmutableList.of( new MPTemplate( "Cvcn" ) ), //
MPResultTypeClass.Template, 0x4 ), MPResultTypeClass.Template, 0x4 ),
/** /**
* 2798 * 2798
*/ */
GeneratedPIN( "PIN", "4 numbers.", // GeneratedPIN( "pin", "4 numbers.", //
ImmutableList.of( new MPTemplate( "nnnn" ) ), // ImmutableList.of( new MPTemplate( "nnnn" ) ), //
MPResultTypeClass.Template, 0x5 ), MPResultTypeClass.Template, 0x5 ),
/** /**
* birsujano * birsujano
*/ */
GeneratedName( "Name", "9 letter name.", // GeneratedName( "name", "9 letter name.", //
ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), // ImmutableList.of( new MPTemplate( "cvccvcvcv" ) ), //
MPResultTypeClass.Template, 0xE ), MPResultTypeClass.Template, 0xE ),
/** /**
* bir yennoquce fefi * bir yennoquce fefi
*/ */
GeneratedPhrase( "Phrase", "20 character sentence.", // GeneratedPhrase( "phrase", "20 character sentence.", //
ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ), ImmutableList.of( new MPTemplate( "cvcc cvc cvccvcv cvc" ),
new MPTemplate( "cvc cvccvcvcv cvcv" ), new MPTemplate( "cvc cvccvcvcv cvcv" ),
new MPTemplate( "cv cvccv cvc cvcvccv" ) ), // new MPTemplate( "cv cvccv cvc cvcvccv" ) ), //
@ -109,25 +109,25 @@ public enum MPResultType {
/** /**
* Custom saved password. * Custom saved password.
*/ */
StoredPersonal( "Personal", "AES-encrypted, exportable.", // StoredPersonal( "personal", "AES-encrypted, exportable.", //
ImmutableList.<MPTemplate>of(), // ImmutableList.<MPTemplate>of(), //
MPResultTypeClass.Stateful, 0x0, MPSiteFeature.ExportContent ), MPResultTypeClass.Stateful, 0x0, MPSiteFeature.ExportContent ),
/** /**
* Custom saved password that should not be exported from the device. * Custom saved password that should not be exported from the device.
*/ */
StoredDevicePrivate( "Device", "AES-encrypted, not exported.", // StoredDevicePrivate( "device", "AES-encrypted, not exported.", //
ImmutableList.<MPTemplate>of(), // ImmutableList.<MPTemplate>of(), //
MPResultTypeClass.Stateful, 0x1, MPSiteFeature.DevicePrivate ), MPResultTypeClass.Stateful, 0x1, MPSiteFeature.DevicePrivate ),
/** /**
* Derive a unique binary key. * Derive a unique binary key.
*/ */
DeriveKey( "Key", "Encryption key.", // DeriveKey( "key", "Encryption key.", //
ImmutableList.<MPTemplate>of(), // ImmutableList.<MPTemplate>of(), //
MPResultTypeClass.Derive, 0x0, MPSiteFeature.Alternative ); MPResultTypeClass.Derive, 0x0, MPSiteFeature.Alternative );
public static MPResultType DEFAULT = GeneratedLong; public static final MPResultType DEFAULT = GeneratedLong;
static final Logger logger = Logger.get( MPResultType.class ); static final Logger logger = Logger.get( MPResultType.class );
@ -246,16 +246,6 @@ public enum MPResultType {
return types.build(); return types.build();
} }
public static MPResultType forInt(final int resultType) {
return values()[resultType];
}
public int toInt() {
return ordinal();
}
public MPTemplate getTemplateAtRollingIndex(final int templateIndex) { public MPTemplate getTemplateAtRollingIndex(final int templateIndex) {
return templates.get( templateIndex % templates.size() ); return templates.get( templateIndex % templates.size() );
} }

View File

@ -20,6 +20,8 @@ package com.lyndir.masterpassword;
import com.google.common.primitives.UnsignedInteger; import com.google.common.primitives.UnsignedInteger;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Arrays;
/** /**
@ -35,6 +37,16 @@ public final class MPUtils {
return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( MPAlgorithmV0.mpw_byteOrder ).putInt( number.intValue() ).array(); return ByteBuffer.allocate( Integer.SIZE / Byte.SIZE ).order( MPAlgorithmV0.mpw_byteOrder ).putInt( number.intValue() ).array();
} }
public static byte[] bytesForChars(final char[] characters) {
ByteBuffer byteBuffer = MPAlgorithmV0.mpw_charset.encode( CharBuffer.wrap( characters ) );
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get( bytes );
Arrays.fill( byteBuffer.array(), (byte) 0 );
return bytes;
}
public static byte[] idForBytes(final byte[] bytes) { public static byte[] idForBytes(final byte[] bytes) {
return MPAlgorithmV0.mpw_hash.of( bytes ); return MPAlgorithmV0.mpw_hash.of( bytes );
} }

View File

@ -171,7 +171,7 @@ public class MPTestSuite implements Callable<Boolean> {
@Override @Override
public boolean run(final MPTests.Case testCase) public boolean run(final MPTests.Case testCase)
throws Exception { throws Exception {
MPMasterKey masterKey = new MPMasterKey( testCase.getFullName(), testCase.getMasterPassword() ); MPMasterKey masterKey = new MPMasterKey( testCase.getFullName(), testCase.getMasterPassword().toCharArray() );
String sitePassword = masterKey.siteResult( testCase.getSiteName(), testCase.getSiteCounter(), testCase.getKeyPurpose(), String sitePassword = masterKey.siteResult( testCase.getSiteName(), testCase.getSiteCounter(), testCase.getKeyPurpose(),
testCase.getKeyContext(), testCase.getResultType(), testCase.getKeyContext(), testCase.getResultType(),
null, testCase.getAlgorithm() ); null, testCase.getAlgorithm() );

View File

@ -181,8 +181,8 @@ public class MPTests {
} }
@Nonnull @Nonnull
public char[] getMasterPassword() { public String getMasterPassword() {
return checkNotNull( masterPassword ).toCharArray(); return checkNotNull( masterPassword );
} }
@Nonnull @Nonnull

View File

@ -22,7 +22,7 @@ import static org.testng.Assert.*;
import com.lyndir.lhunath.opal.system.CodeUtils; import com.lyndir.lhunath.opal.system.CodeUtils;
import com.lyndir.lhunath.opal.system.logging.Logger; import com.lyndir.lhunath.opal.system.logging.Logger;
import org.jetbrains.annotations.NonNls; import java.util.Random;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
@ -32,7 +32,6 @@ public class MPMasterKeyTest {
@SuppressWarnings("UnusedDeclaration") @SuppressWarnings("UnusedDeclaration")
private static final Logger logger = Logger.get( MPMasterKeyTest.class ); private static final Logger logger = Logger.get( MPMasterKeyTest.class );
@NonNls
private MPTestSuite testSuite; private MPTestSuite testSuite;
@BeforeMethod @BeforeMethod
@ -43,20 +42,58 @@ public class MPMasterKeyTest {
} }
@Test @Test
public void testEncode() public void testMasterKey()
throws Exception { throws Exception {
testSuite.forEach( "testEncode", new MPTestSuite.TestCase() { testSuite.forEach( "testMasterKey", new MPTestSuite.TestCase() {
@Override @Override
public boolean run(final MPTests.Case testCase) public boolean run(final MPTests.Case testCase)
throws Exception { throws Exception {
MPMasterKey masterKey = new MPMasterKey( testCase.getFullName(), testCase.getMasterPassword() ); char[] masterPassword = testCase.getMasterPassword().toCharArray();
MPMasterKey masterKey = new MPMasterKey( testCase.getFullName(), masterPassword );
// Test key
assertEquals(
CodeUtils.encodeHex( masterKey.getKeyID( testCase.getAlgorithm() ) ),
testCase.getKeyID(),
"[testMasterKey] keyID mismatch: " + testCase );
// Test invalidation
masterKey.invalidate();
try {
masterKey.getKeyID( testCase.getAlgorithm() );
fail( "[testMasterKey] invalidate ineffective: " + testCase );
}
catch (final MPInvalidatedException ignored) {
}
assertNotEquals(
masterPassword,
testCase.getMasterPassword().toCharArray(),
"[testMasterKey] masterPassword not wiped: " + testCase );
return true;
}
} );
}
@Test
public void testSiteResult()
throws Exception {
testSuite.forEach( "testSiteResult", new MPTestSuite.TestCase() {
@Override
public boolean run(final MPTests.Case testCase)
throws Exception {
char[] masterPassword = testCase.getMasterPassword().toCharArray();
MPMasterKey masterKey = new MPMasterKey( testCase.getFullName(), masterPassword );
// Test site result
assertEquals( assertEquals(
masterKey.siteResult( testCase.getSiteName(), testCase.getSiteCounter(), testCase.getKeyPurpose(), masterKey.siteResult( testCase.getSiteName(), testCase.getSiteCounter(), testCase.getKeyPurpose(),
testCase.getKeyContext(), testCase.getResultType(), testCase.getKeyContext(), testCase.getResultType(),
null, testCase.getAlgorithm() ), null, testCase.getAlgorithm() ),
testCase.getResult(), "[testEncode] Failed test case: " + testCase ); testCase.getResult(),
"[testSiteResult] result mismatch: " + testCase );
return true; return true;
} }
@ -64,30 +101,30 @@ public class MPMasterKeyTest {
} }
@Test @Test
public void testGetUserName() public void testSiteState()
throws Exception { throws Exception {
MPTests.Case defaultCase = testSuite.getTests().getDefaultCase(); MPTests.Case testCase = testSuite.getTests().getDefaultCase();
char[] masterPassword = testCase.getMasterPassword().toCharArray();
MPMasterKey masterKey = new MPMasterKey( testCase.getFullName(), masterPassword );
assertEquals( new MPMasterKey( defaultCase.getFullName(), defaultCase.getMasterPassword() ).getFullName(), Random random = new Random();
defaultCase.getFullName(), "[testGetUserName] Failed test case: " + defaultCase ); StringBuilder password = new StringBuilder();
} for (int p = 0; p < 8; ++p)
password.append( (char) (random.nextInt( Character.MAX_CODE_POINT - Character.MIN_CODE_POINT ) + Character.MIN_CODE_POINT) );
@Test for (final MPMasterKey.Version version : MPMasterKey.Version.values()) {
public void testGetKeyID() MPResultType resultType = MPResultType.StoredPersonal;
throws Exception {
testSuite.forEach( "testGetKeyID", new MPTestSuite.TestCase() { // Test site state
@Override String state = masterKey.siteState( testCase.getSiteName(), testCase.getSiteCounter(), testCase.getKeyPurpose(),
public boolean run(final MPTests.Case testCase) testCase.getKeyContext(), resultType, password.toString(), version );
throws Exception {
MPMasterKey masterKey = new MPMasterKey( testCase.getFullName(), testCase.getMasterPassword() );
assertEquals( CodeUtils.encodeHex( masterKey.getKeyID( testCase.getAlgorithm() ) ), assertEquals(
testCase.getKeyID(), "[testGetKeyID] Failed test case: " + testCase ); masterKey.siteResult( testCase.getSiteName(), testCase.getSiteCounter(), testCase.getKeyPurpose(),
testCase.getKeyContext(), resultType, state, version ),
return true; password.toString(),
} "[testSiteState] state mismatch: " + testCase );
} ); }
} }
} }

View File

@ -6,9 +6,9 @@
</encoder> </encoder>
</appender> </appender>
<logger name="com.lyndir" level="${mp.log.level:-INFO}" /> <logger name="com.lyndir" level="${mp.log.level:-TRACE}" />
<root level="INFO"> <root level="TRACE">
<appender-ref ref="stdout" /> <appender-ref ref="stdout" />
</root> </root>

View File

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="GUI" type="Application" factoryName="Application"> <configuration default="false" name="GUI" type="Application" factoryName="Application" show_console_on_std_err="true">
<option name="MAIN_CLASS_NAME" value="com.lyndir.masterpassword.gui.GUI" /> <option name="MAIN_CLASS_NAME" value="com.lyndir.masterpassword.gui.GUI" />
<option name="VM_PARAMETERS" value="" /> <option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" /> <option name="PROGRAM_PARAMETERS" value="" />
@ -9,7 +9,7 @@
<option name="ENABLE_SWING_INSPECTOR" value="false" /> <option name="ENABLE_SWING_INSPECTOR" value="false" />
<option name="ENV_VARIABLES" /> <option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" /> <option name="PASS_PARENT_ENVS" value="true" />
<module name="gui" /> <module name="masterpassword-gui" />
<envs /> <envs />
<method /> <method />
</configuration> </configuration>

View File

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Tests" type="TestNG" factoryName="TestNG"> <configuration default="false" name="Tests" type="TestNG" factoryName="TestNG" show_console_on_std_err="true">
<module name="tests" /> <module name="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" /> <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" /> <option name="ALTERNATIVE_JRE_PATH" />
<option name="SUITE_NAME" value="" /> <option name="SUITE_NAME" value="" />
@ -17,7 +17,7 @@
<option name="ENV_VARIABLES" /> <option name="ENV_VARIABLES" />
<option name="PASS_PARENT_ENVS" value="true" /> <option name="PASS_PARENT_ENVS" value="true" />
<option name="TEST_SEARCH_SCOPE"> <option name="TEST_SEARCH_SCOPE">
<value defaultName="singleModule" /> <value defaultName="wholeProject" />
</option> </option>
<option name="USE_DEFAULT_REPORTERS" value="false" /> <option name="USE_DEFAULT_REPORTERS" value="false" />
<option name="PROPERTIES_FILE" value="" /> <option name="PROPERTIES_FILE" value="" />