diff --git a/README.md b/README.md
index 290e66c8..5b8898a1 100644
--- a/README.md
+++ b/README.md
@@ -23,11 +23,11 @@ Features
- Optical character recognition
- Support image, PDF, ODT and DOCX files
- Flexible search engine
-- Full text search in image and PDF
+- Full text search in all supported files
- All [Dublin Core](http://dublincore.org/) metadata
-- 256-bit AES encryption
-- Tag system with relations
-- Multi-users ACL system
+- 256-bit AES encryption of stored files
+- Tag system with nesting
+- User/group permission system
- Hierarchical groups
- Audit log
- Comments
diff --git a/docs-core/pom.xml b/docs-core/pom.xml
index ed0673dc..eeb23ba9 100644
--- a/docs-core/pom.xml
+++ b/docs-core/pom.xml
@@ -77,7 +77,6 @@
jbcrypt
-
org.apache.lucenelucene-core
diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java
index 92cb1718..bc392947 100644
--- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java
+++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/UserDao.java
@@ -36,9 +36,9 @@ public class UserDao {
*
* @param username User login
* @param password User password
- * @return ID of the authenticated user or null
+ * @return The authenticated user or null
*/
- public String authenticate(String username, String password) {
+ public User authenticate(String username, String password) {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query q = em.createQuery("select u from User u where u.username = :username and u.deleteDate is null");
q.setParameter("username", username);
@@ -47,7 +47,7 @@ public class UserDao {
if (!BCrypt.checkpw(password, user.getPassword())) {
return null;
}
- return user.getId();
+ return user;
} catch (NoResultException e) {
return null;
}
@@ -104,6 +104,7 @@ public class UserDao {
userFromDb.setEmail(user.getEmail());
userFromDb.setStorageQuota(user.getStorageQuota());
userFromDb.setStorageCurrent(user.getStorageCurrent());
+ userFromDb.setTotpKey(user.getTotpKey());
// Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId);
diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java
index c5030e0d..3b592bf2 100644
--- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java
+++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java
@@ -64,56 +64,63 @@ public class AuthenticationToken {
return id;
}
- public void setId(String id) {
+ public AuthenticationToken setId(String id) {
this.id = id;
+ return this;
}
public String getUserId() {
return userId;
}
- public void setUserId(String userId) {
+ public AuthenticationToken setUserId(String userId) {
this.userId = userId;
+ return this;
}
public boolean isLongLasted() {
return longLasted;
}
- public void setLongLasted(boolean longLasted) {
+ public AuthenticationToken setLongLasted(boolean longLasted) {
this.longLasted = longLasted;
+ return this;
}
public String getIp() {
return ip;
}
- public void setIp(String ip) {
+ public AuthenticationToken setIp(String ip) {
this.ip = ip;
+ return this;
}
public String getUserAgent() {
return userAgent;
}
- public void setUserAgent(String userAgent) {
+ public AuthenticationToken setUserAgent(String userAgent) {
this.userAgent = userAgent;
+ return this;
}
public Date getCreationDate() {
return creationDate;
}
- public void setCreationDate(Date creationDate) {
+ public AuthenticationToken setCreationDate(Date creationDate) {
this.creationDate = creationDate;
+ return this;
}
public Date getLastConnectionDate() {
return lastConnectionDate;
}
- public void setLastConnectionDate(Date lastConnectionDate) {
+ public AuthenticationToken setLastConnectionDate(Date lastConnectionDate) {
this.lastConnectionDate = lastConnectionDate;
+ return this;
}
@Override
diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java
index 5476a4ab..0f37b50c 100644
--- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java
+++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java
@@ -48,6 +48,12 @@ public class User implements Loggable {
@Column(name = "USE_PRIVATEKEY_C", nullable = false, length = 100)
private String privateKey;
+ /**
+ * TOTP secret key.
+ */
+ @Column(name = "USE_TOTPKEY_C", length = 100)
+ private String totpKey;
+
/**
* Email address.
*/
@@ -82,48 +88,54 @@ public class User implements Loggable {
return id;
}
- public void setId(String id) {
+ public User setId(String id) {
this.id = id;
+ return this;
}
public String getRoleId() {
return roleId;
}
- public void setRoleId(String roleId) {
+ public User setRoleId(String roleId) {
this.roleId = roleId;
+ return this;
}
public String getUsername() {
return username;
}
- public void setUsername(String username) {
+ public User setUsername(String username) {
this.username = username;
+ return this;
}
public String getPassword() {
return password;
}
- public void setPassword(String password) {
+ public User setPassword(String password) {
this.password = password;
+ return this;
}
public String getEmail() {
return email;
}
- public void setEmail(String email) {
+ public User setEmail(String email) {
this.email = email;
+ return this;
}
public Date getCreateDate() {
return createDate;
}
- public void setCreateDate(Date createDate) {
+ public User setCreateDate(Date createDate) {
this.createDate = createDate;
+ return this;
}
@Override
@@ -131,32 +143,45 @@ public class User implements Loggable {
return deleteDate;
}
- public void setDeleteDate(Date deleteDate) {
+ public User setDeleteDate(Date deleteDate) {
this.deleteDate = deleteDate;
+ return this;
}
public String getPrivateKey() {
return privateKey;
}
- public void setPrivateKey(String privateKey) {
+ public User setPrivateKey(String privateKey) {
this.privateKey = privateKey;
+ return this;
}
public Long getStorageQuota() {
return storageQuota;
}
- public void setStorageQuota(Long storageQuota) {
+ public User setStorageQuota(Long storageQuota) {
this.storageQuota = storageQuota;
+ return this;
}
public Long getStorageCurrent() {
return storageCurrent;
}
- public void setStorageCurrent(Long storageCurrent) {
+ public User setStorageCurrent(Long storageCurrent) {
this.storageCurrent = storageCurrent;
+ return this;
+ }
+
+ public String getTotpKey() {
+ return totpKey;
+ }
+
+ public User setTotpKey(String totpKey) {
+ this.totpKey = totpKey;
+ return this;
}
@Override
diff --git a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java
new file mode 100644
index 00000000..c37147f9
--- /dev/null
+++ b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticator.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (c) 2014-2016 Enrico M. Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * * Neither the name of the author nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.sismics.util.totp;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.codec.binary.Base32;
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ * This class implements the functionality described in RFC 6238 (TOTP: Time
+ * based one-time password algorithm) and has been tested again Google's
+ * implementation of such algorithm in its Google Authenticator application.
+ *
+ * This class lets users create a new 16-bit base32-encoded secret key with the
+ * validation code calculated at {@code time = 0} (the UNIX epoch) and the URL
+ * of a Google-provided QR barcode to let an user load the generated information
+ * into Google Authenticator.
+ *
+ * The random number generator used by this class uses the default algorithm and
+ * provider. Users can override them by setting the following system properties
+ * to the algorithm and provider name of their choice:
+ *
+ *
{@link #RNG_ALGORITHM}.
+ *
{@link #RNG_ALGORITHM_PROVIDER}.
+ *
+ *
+ * This class does not store in any way either the generated keys nor the keys
+ * passed during the authorization process.
+ *
+ * Java Server side class for Google Authenticator's TOTP generator was inspired
+ * by an author's blog post.
+ *
+ * @author Enrico M. Crisostomo
+ * @author Warren Strange
+ * @version 0.5.0
+ * @see
+ * @see
+ * @see
+ * @since 0.3.0
+ */
+public final class GoogleAuthenticator {
+ /**
+ * The system property to specify the random number generator algorithm to
+ * use.
+ *
+ * @since 0.5.0
+ */
+ public static final String RNG_ALGORITHM = "com.warrenstrange.googleauth.rng.algorithm";
+
+ /**
+ * The system property to specify the random number generator provider to
+ * use.
+ *
+ * @since 0.5.0
+ */
+ public static final String RNG_ALGORITHM_PROVIDER = "com.warrenstrange.googleauth.rng.algorithmProvider";
+
+ /**
+ * The number of bits of a secret key in binary form. Since the Base32
+ * encoding with 8 bit characters introduces an 160% overhead, we just need
+ * 80 bits (10 bytes) to generate a 16 bytes Base32-encoded secret key.
+ */
+ private static final int SECRET_BITS = 80;
+
+ /**
+ * Number of scratch codes to generate during the key generation. We are
+ * using Google's default of providing 5 scratch codes.
+ */
+ private static final int SCRATCH_CODES = 5;
+
+ /**
+ * Number of digits of a scratch code represented as a decimal integer.
+ */
+ private static final int SCRATCH_CODE_LENGTH = 8;
+
+ /**
+ * Modulus used to truncate the scratch code.
+ */
+ public static final int SCRATCH_CODE_MODULUS = (int) Math.pow(10, SCRATCH_CODE_LENGTH);
+
+ /**
+ * Magic number representing an invalid scratch code.
+ */
+ private static final int SCRATCH_CODE_INVALID = -1;
+
+ /**
+ * Length in bytes of each scratch code. We're using Google's default of
+ * using 4 bytes per scratch code.
+ */
+ private static final int BYTES_PER_SCRATCH_CODE = 4;
+
+ /**
+ * The default SecureRandom algorithm to use if none is specified.
+ *
+ * @see java.security.SecureRandom#getInstance(String)
+ * @since 0.5.0
+ */
+ private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM = "SHA1PRNG";
+
+ /**
+ * The default random number algorithm provider to use if none is specified.
+ *
+ * @see java.security.SecureRandom#getInstance(String)
+ * @since 0.5.0
+ */
+ private static final String DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER = "SUN";
+
+ /**
+ * Cryptographic hash function used to calculate the HMAC (Hash-based
+ * Message Authentication Code). This implementation uses the SHA1 hash
+ * function.
+ */
+ private static final String HMAC_HASH_FUNCTION = "HmacSHA1";
+
+ /**
+ * The configuration used by the current instance.
+ */
+ private final GoogleAuthenticatorConfig config;
+
+ /**
+ * The internal SecureRandom instance used by this class. Since Java 7
+ * {@link Random} instances are required to be thread-safe, no
+ * synchronisation is required in the methods of this class using this
+ * instance. Thread-safety of this class was a de-facto standard in previous
+ * versions of Java so that it is expected to work correctly in previous
+ * versions of the Java platform as well.
+ */
+ private ReseedingSecureRandom secureRandom = new ReseedingSecureRandom(getRandomNumberAlgorithm(), getRandomNumberAlgorithmProvider());
+
+ public GoogleAuthenticator() {
+ config = new GoogleAuthenticatorConfig();
+ }
+
+ public GoogleAuthenticator(GoogleAuthenticatorConfig config) {
+ if (config == null) {
+ throw new IllegalArgumentException("Configuration cannot be null.");
+ }
+
+ this.config = config;
+ }
+
+ /**
+ * @return the random number generator algorithm.
+ * @since 0.5.0
+ */
+ private String getRandomNumberAlgorithm() {
+ return System.getProperty(RNG_ALGORITHM, DEFAULT_RANDOM_NUMBER_ALGORITHM);
+ }
+
+ /**
+ * @return the random number generator algorithm provider.
+ * @since 0.5.0
+ */
+ private String getRandomNumberAlgorithmProvider() {
+ return System.getProperty(RNG_ALGORITHM_PROVIDER, DEFAULT_RANDOM_NUMBER_ALGORITHM_PROVIDER);
+ }
+
+ public int calculateCode(String secret, long tm) {
+ return calculateCode(decodeKey(secret), tm);
+ }
+
+ /**
+ * Decode a secret key in raw bytes.
+ *
+ * @param secret Secret key
+ * @return Raw bytes
+ */
+ private byte[] decodeKey(String secret) {
+ switch (config.getKeyRepresentation()) {
+ case BASE32:
+ Base32 codec32 = new Base32();
+ return codec32.decode(secret);
+ case BASE64:
+ Base64 codec64 = new Base64();
+ return codec64.decode(secret);
+ default:
+ throw new IllegalArgumentException("Unknown key representation type.");
+ }
+ }
+
+ /**
+ * Calculates the verification code of the provided key at the specified
+ * instant of time using the algorithm specified in RFC 6238.
+ *
+ * @param key the secret key in binary format.
+ * @param tm the instant of time.
+ * @return the validation code for the provided key at the specified instant
+ * of time.
+ */
+ int calculateCode(byte[] key, long tm) {
+ // Allocating an array of bytes to represent the specified instant
+ // of time.
+ byte[] data = new byte[8];
+ long value = tm;
+
+ // Converting the instant of time from the long representation to a
+ // big-endian array of bytes (RFC4226, 5.2. Description).
+ for (int i = 8; i-- > 0; value >>>= 8) {
+ data[i] = (byte) value;
+ }
+
+ // Building the secret key specification for the HmacSHA1 algorithm.
+ SecretKeySpec signKey = new SecretKeySpec(key, HMAC_HASH_FUNCTION);
+
+ try {
+ // Getting an HmacSHA1 algorithm implementation from the JCE.
+ Mac mac = Mac.getInstance(HMAC_HASH_FUNCTION);
+
+ // Initializing the MAC algorithm.
+ mac.init(signKey);
+
+ // Processing the instant of time and getting the encrypted data.
+ byte[] hash = mac.doFinal(data);
+
+ // Building the validation code performing dynamic truncation
+ // (RFC4226, 5.3. Generating an HOTP value)
+ int offset = hash[hash.length - 1] & 0xF;
+
+ // We are using a long because Java hasn't got an unsigned integer
+ // type
+ // and we need 32 unsigned bits).
+ long truncatedHash = 0;
+
+ for (int i = 0; i < 4; ++i) {
+ truncatedHash <<= 8;
+
+ // Java bytes are signed but we need an unsigned integer:
+ // cleaning off all but the LSB.
+ truncatedHash |= (hash[offset + i] & 0xFF);
+ }
+
+ // Clean bits higher than the 32nd (inclusive) and calculate the
+ // module with the maximum validation code value.
+ truncatedHash &= 0x7FFFFFFF;
+ truncatedHash %= config.getKeyModulus();
+
+ // Returning the validation code to the caller.
+ return (int) truncatedHash;
+ } catch (NoSuchAlgorithmException | InvalidKeyException ex) {
+ // We're not disclosing internal error details to our clients.
+ throw new GoogleAuthenticatorException("The operation cannot be performed now.", ex);
+ }
+ }
+
+ /**
+ * This method implements the algorithm specified in RFC 6238 to check if a
+ * validation code is valid in a given instant of time for the given secret
+ * key.
+ *
+ * @param secret the Base32 encoded secret key.
+ * @param code the code to validate.
+ * @param timestamp the instant of time to use during the validation process.
+ * @param window the window size to use during the validation process.
+ * @return true if the validation code is valid,
+ * false otherwise.
+ */
+ private boolean checkCode(String secret, long code, long timestamp, int window) {
+ // Decoding the secret key to get its raw byte representation.
+ byte[] decodedKey = decodeKey(secret);
+
+ // convert unix time into a 30 second "window" as specified by the
+ // TOTP specification. Using Google's default interval of 30 seconds.
+ final long timeWindow = timestamp / this.config.getTimeStepSizeInMillis();
+
+ // Calculating the verification code of the given key in each of the
+ // time intervals and returning true if the provided code is equal to
+ // one of them.
+ for (int i = -((window - 1) / 2); i <= window / 2; ++i) {
+ // Calculating the verification code for the current time interval.
+ long hash = calculateCode(decodedKey, timeWindow + i);
+
+ // Checking if the provided code is equal to the calculated one.
+ if (hash == code) {
+ // The verification code is valid.
+ return true;
+ }
+ }
+
+ // The verification code is invalid.
+ return false;
+ }
+
+ public GoogleAuthenticatorKey createCredentials() {
+ // Allocating a buffer sufficiently large to hold the bytes required by
+ // the secret key and the scratch codes.
+ byte[] buffer = new byte[SECRET_BITS / 8 + SCRATCH_CODES * BYTES_PER_SCRATCH_CODE];
+
+ secureRandom.nextBytes(buffer);
+
+ // Extracting the bytes making up the secret key.
+ byte[] secretKey = Arrays.copyOf(buffer, SECRET_BITS / 8);
+ String generatedKey = calculateSecretKey(secretKey);
+
+ // Generating the verification code at time = 0.
+ int validationCode = calculateValidationCode(secretKey);
+
+ // Calculate scratch codes
+ List scratchCodes = calculateScratchCodes(buffer);
+
+ return new GoogleAuthenticatorKey(generatedKey, validationCode, scratchCodes);
+ }
+
+ private List calculateScratchCodes(byte[] buffer) {
+ List scratchCodes = new ArrayList<>();
+
+ while (scratchCodes.size() < SCRATCH_CODES) {
+ byte[] scratchCodeBuffer = Arrays.copyOfRange(buffer, SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size(), SECRET_BITS / 8 + BYTES_PER_SCRATCH_CODE * scratchCodes.size() + BYTES_PER_SCRATCH_CODE);
+
+ int scratchCode = calculateScratchCode(scratchCodeBuffer);
+
+ if (scratchCode != SCRATCH_CODE_INVALID) {
+ scratchCodes.add(scratchCode);
+ } else {
+ scratchCodes.add(generateScratchCode());
+ }
+ }
+
+ return scratchCodes;
+ }
+
+ /**
+ * This method calculates a scratch code from a random byte buffer of
+ * suitable size #BYTES_PER_SCRATCH_CODE.
+ *
+ * @param scratchCodeBuffer a random byte buffer whose minimum size is #BYTES_PER_SCRATCH_CODE.
+ * @return the scratch code.
+ */
+ private int calculateScratchCode(byte[] scratchCodeBuffer) {
+ if (scratchCodeBuffer.length < BYTES_PER_SCRATCH_CODE) {
+ throw new IllegalArgumentException(String.format("The provided random byte buffer is too small: %d.", scratchCodeBuffer.length));
+ }
+
+ int scratchCode = 0;
+
+ for (int i = 0; i < BYTES_PER_SCRATCH_CODE; ++i) {
+ scratchCode = (scratchCode << 8) + (scratchCodeBuffer[i] & 0xff);
+ }
+
+ scratchCode = (scratchCode & 0x7FFFFFFF) % SCRATCH_CODE_MODULUS;
+
+ // Accept the scratch code only if it has exactly
+ // SCRATCH_CODE_LENGTH digits.
+ if (scratchCode >= SCRATCH_CODE_MODULUS / 10) {
+ return scratchCode;
+ } else {
+ return SCRATCH_CODE_INVALID;
+ }
+ }
+
+ /**
+ * This method creates a new random byte buffer from which a new scratch
+ * code is generated. This function is invoked if a scratch code generated
+ * from the main buffer is invalid because it does not satisfy the scratch
+ * code restrictions.
+ *
+ * @return A valid scratch code.
+ */
+ private int generateScratchCode() {
+ while (true) {
+ byte[] scratchCodeBuffer = new byte[BYTES_PER_SCRATCH_CODE];
+ secureRandom.nextBytes(scratchCodeBuffer);
+
+ int scratchCode = calculateScratchCode(scratchCodeBuffer);
+
+ if (scratchCode != SCRATCH_CODE_INVALID) {
+ return scratchCode;
+ }
+ }
+ }
+
+ /**
+ * This method calculates the validation code at time 0.
+ *
+ * @param secretKey The secret key to use.
+ * @return the validation code at time 0.
+ */
+ private int calculateValidationCode(byte[] secretKey) {
+ return calculateCode(secretKey, 0);
+ }
+
+ /**
+ * This method calculates the secret key given a random byte buffer.
+ *
+ * @param secretKey a random byte buffer.
+ * @return the secret key.
+ */
+ private String calculateSecretKey(byte[] secretKey) {
+ switch (config.getKeyRepresentation()) {
+ case BASE32:
+ return new Base32().encodeToString(secretKey);
+ case BASE64:
+ return new Base64().encodeToString(secretKey);
+ default:
+ throw new IllegalArgumentException("Unknown key representation type.");
+ }
+ }
+
+ public boolean authorize(String secret, int verificationCode) throws GoogleAuthenticatorException {
+ return authorize(secret, verificationCode, new Date().getTime());
+ }
+
+ public boolean authorize(String secret, int verificationCode, long time) throws GoogleAuthenticatorException {
+ // Checking user input and failing if the secret key was not provided.
+ if (secret == null) {
+ throw new IllegalArgumentException("Secret cannot be null.");
+ }
+
+ // Checking if the verification code is between the legal bounds.
+ if (verificationCode <= 0 || verificationCode >= this.config.getKeyModulus()) {
+ return false;
+ }
+
+ // Checking the validation code using the current UNIX time.
+ return checkCode(secret, verificationCode, time, this.config.getWindowSize());
+ }
+}
diff --git a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorConfig.java b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorConfig.java
new file mode 100644
index 00000000..cabc7f63
--- /dev/null
+++ b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorConfig.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2014-2015 Enrico M. Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * * Neither the name of the author nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.sismics.util.totp;
+
+import java.util.concurrent.TimeUnit;
+
+public class GoogleAuthenticatorConfig {
+ private long timeStepSizeInMillis = TimeUnit.SECONDS.toMillis(30);
+ private int windowSize = 3;
+ private int codeDigits = 6;
+ private int keyModulus = (int) Math.pow(10, codeDigits);
+ private KeyRepresentation keyRepresentation = KeyRepresentation.BASE32;
+
+ /**
+ * Returns the key module.
+ *
+ * @return the key module.
+ */
+ public int getKeyModulus() {
+ return keyModulus;
+ }
+
+ /**
+ * Returns the key representation.
+ *
+ * @return the key representation.
+ */
+ public KeyRepresentation getKeyRepresentation() {
+ return keyRepresentation;
+ }
+
+ /**
+ * Returns the number of digits in the generated code.
+ *
+ * @return the number of digits in the generated code.
+ */
+ public int getCodeDigits() {
+ return codeDigits;
+ }
+
+ /**
+ * Returns the time step size, in milliseconds, as specified by RFC 6238.
+ * The default value is 30.000.
+ *
+ * @return the time step size in milliseconds.
+ */
+ public long getTimeStepSizeInMillis() {
+ return timeStepSizeInMillis;
+ }
+
+ /**
+ * Returns an integer value representing the number of windows of size
+ * timeStepSizeInMillis that are checked during the validation process, to
+ * account for differences between the server and the client clocks. The
+ * bigger the window, the more tolerant the library code is about clock
+ * skews.
+ *
+ * We are using Google's default behaviour of using a window size equal to
+ * 3. The limit on the maximum window size, present in older versions of
+ * this library, has been removed.
+ *
+ * @return the window size.
+ * @see #timeStepSizeInMillis
+ */
+ public int getWindowSize() {
+ return windowSize;
+ }
+
+ public static class GoogleAuthenticatorConfigBuilder {
+ private GoogleAuthenticatorConfig config = new GoogleAuthenticatorConfig();
+
+ public GoogleAuthenticatorConfig build() {
+ return config;
+ }
+
+ public GoogleAuthenticatorConfigBuilder setCodeDigits(int codeDigits) {
+ if (codeDigits <= 0) {
+ throw new IllegalArgumentException("Code digits must be positive.");
+ }
+
+ if (codeDigits < 6) {
+ throw new IllegalArgumentException("The minimum number of digits is 6.");
+ }
+
+ if (codeDigits > 8) {
+ throw new IllegalArgumentException("The maximum number of digits is 8.");
+ }
+
+ config.codeDigits = codeDigits;
+ config.keyModulus = (int) Math.pow(10, codeDigits);
+ return this;
+ }
+
+ public GoogleAuthenticatorConfigBuilder setTimeStepSizeInMillis(long timeStepSizeInMillis) {
+ if (timeStepSizeInMillis <= 0) {
+ throw new IllegalArgumentException("Time step size must be positive.");
+ }
+
+ config.timeStepSizeInMillis = timeStepSizeInMillis;
+ return this;
+ }
+
+ public GoogleAuthenticatorConfigBuilder setWindowSize(int windowSize) {
+ if (windowSize <= 0) {
+ throw new IllegalArgumentException("Window number must be positive.");
+ }
+
+ config.windowSize = windowSize;
+ return this;
+ }
+
+ public GoogleAuthenticatorConfigBuilder setKeyRepresentation(KeyRepresentation keyRepresentation) {
+ if (keyRepresentation == null) {
+ throw new IllegalArgumentException("Key representation cannot be null.");
+ }
+
+ config.keyRepresentation = keyRepresentation;
+ return this;
+ }
+ }
+}
diff --git a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorException.java b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorException.java
new file mode 100644
index 00000000..77a742b8
--- /dev/null
+++ b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorException.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2014-2015 Enrico M. Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * * Neither the name of the author nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.sismics.util.totp;
+
+/**
+ * Date: 12/02/14
+ * Time: 13:36
+ *
+ * @author Enrico M. Crisostomo
+ */
+public class GoogleAuthenticatorException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Builds an exception with the provided error message.
+ *
+ * @param message the error message.
+ */
+ public GoogleAuthenticatorException(String message) {
+ super(message);
+ }
+
+ /**
+ * Builds an exception with the provided error mesasge and
+ * the provided cuase.
+ *
+ * @param message the error message.
+ * @param cause the cause.
+ */
+ public GoogleAuthenticatorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorKey.java b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorKey.java
new file mode 100644
index 00000000..ee687153
--- /dev/null
+++ b/docs-core/src/main/java/com/sismics/util/totp/GoogleAuthenticatorKey.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2014-2015 Enrico M. Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * * Neither the name of the author nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.sismics.util.totp;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class is a JavaBean used by the GoogleAuthenticator library to represent
+ * a secret key.
+ *
+ * This class is immutable.
+ *
+ * Instance of this class should only be constructed by the GoogleAuthenticator
+ * library.
+ *
+ * @author Enrico M. Crisostomo
+ * @version 1.0
+ * @see GoogleAuthenticator
+ * @since 1.0
+ */
+public final class GoogleAuthenticatorKey {
+ /**
+ * The secret key in Base32 encoding.
+ */
+ private final String key;
+
+ /**
+ * The verification code at time = 0 (the UNIX epoch).
+ */
+ private final int verificationCode;
+
+ /**
+ * The list of scratch codes.
+ */
+ private final List scratchCodes;
+
+ /**
+ * The constructor with package visibility.
+ *
+ * @param secretKey the secret key in Base32 encoding.
+ * @param code the verification code at time = 0 (the UNIX epoch).
+ * @param scratchCodes the list of scratch codes.
+ */
+ GoogleAuthenticatorKey(String secretKey, int code, List scratchCodes) {
+ key = secretKey;
+ verificationCode = code;
+ this.scratchCodes = new ArrayList<>(scratchCodes);
+ }
+
+ /**
+ * Get the list of scratch codes.
+ *
+ * @return the list of scratch codes.
+ */
+ public List getScratchCodes() {
+ return scratchCodes;
+ }
+
+ /**
+ * Returns the secret key in Base32 encoding.
+ *
+ * @return the secret key in Base32 encoding.
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Returns the verification code at time = 0 (the UNIX epoch).
+ *
+ * @return the verificationCode at time = 0 (the UNIX epoch).
+ */
+ public int getVerificationCode() {
+ return verificationCode;
+ }
+}
diff --git a/docs-core/src/main/java/com/sismics/util/totp/KeyRepresentation.java b/docs-core/src/main/java/com/sismics/util/totp/KeyRepresentation.java
new file mode 100644
index 00000000..9461ba08
--- /dev/null
+++ b/docs-core/src/main/java/com/sismics/util/totp/KeyRepresentation.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2014-2015 Enrico M. Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * * Neither the name of the author nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.sismics.util.totp;
+
+public enum KeyRepresentation {
+ BASE32,
+ BASE64
+}
diff --git a/docs-core/src/main/java/com/sismics/util/totp/ReseedingSecureRandom.java b/docs-core/src/main/java/com/sismics/util/totp/ReseedingSecureRandom.java
new file mode 100644
index 00000000..fd135d6f
--- /dev/null
+++ b/docs-core/src/main/java/com/sismics/util/totp/ReseedingSecureRandom.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2014-2015 Enrico M. Crisostomo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * * Neither the name of the author nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.sismics.util.totp;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Date: 08/04/14 Time: 15:21
+ *
+ * @author Enrico M. Crisostomo
+ */
+class ReseedingSecureRandom {
+ private static final int MAX_OPERATIONS = 1_000_000;
+ private final String provider;
+ private final String algorithm;
+ private final AtomicInteger count = new AtomicInteger(0);
+ private SecureRandom secureRandom;
+
+ ReseedingSecureRandom() {
+ this.algorithm = null;
+ this.provider = null;
+
+ buildSecureRandom();
+ }
+
+ ReseedingSecureRandom(String algorithm) {
+ if (algorithm == null) {
+ throw new IllegalArgumentException("Algorithm cannot be null.");
+ }
+
+ this.algorithm = algorithm;
+ this.provider = null;
+
+ buildSecureRandom();
+ }
+
+ ReseedingSecureRandom(String algorithm, String provider) {
+ if (algorithm == null) {
+ throw new IllegalArgumentException("Algorithm cannot be null.");
+ }
+
+ if (provider == null) {
+ throw new IllegalArgumentException("Provider cannot be null.");
+ }
+
+ this.algorithm = algorithm;
+ this.provider = provider;
+
+ buildSecureRandom();
+ }
+
+ private void buildSecureRandom() {
+ try {
+ if (this.algorithm == null && this.provider == null) {
+ this.secureRandom = new SecureRandom();
+ } else if (this.provider == null) {
+ this.secureRandom = SecureRandom.getInstance(this.algorithm);
+ } else {
+ this.secureRandom = SecureRandom.getInstance(this.algorithm, this.provider);
+ }
+ } catch (NoSuchAlgorithmException e) {
+ throw new GoogleAuthenticatorException(String.format("Could not initialise SecureRandom with the specified algorithm: %s. "
+ + "Another provider can be chosen setting the %s system property.", this.algorithm, GoogleAuthenticator.RNG_ALGORITHM), e);
+ } catch (NoSuchProviderException e) {
+ throw new GoogleAuthenticatorException(String.format("Could not initialise SecureRandom with the specified provider: %s. "
+ + "Another provider can be chosen setting the %s system property.", this.provider, GoogleAuthenticator.RNG_ALGORITHM_PROVIDER), e);
+ }
+ }
+
+ void nextBytes(byte[] bytes) {
+ if (count.incrementAndGet() > MAX_OPERATIONS) {
+ synchronized (this) {
+ if (count.get() > MAX_OPERATIONS) {
+ buildSecureRandom();
+ count.set(0);
+ }
+ }
+ }
+
+ this.secureRandom.nextBytes(bytes);
+ }
+}
diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties
index fda5dfd0..edf8e6a4 100644
--- a/docs-core/src/main/resources/config.properties
+++ b/docs-core/src/main/resources/config.properties
@@ -1 +1 @@
-db.version=8
\ No newline at end of file
+db.version=9
\ No newline at end of file
diff --git a/docs-core/src/main/resources/db/update/dbupdate-009-0.sql b/docs-core/src/main/resources/db/update/dbupdate-009-0.sql
new file mode 100644
index 00000000..2b5e8ea1
--- /dev/null
+++ b/docs-core/src/main/resources/db/update/dbupdate-009-0.sql
@@ -0,0 +1,2 @@
+alter table T_USER add column USE_TOTPKEY_C varchar(100);
+update T_CONFIG set CFG_VALUE_C = '9' where CFG_ID_C = 'DB_VERSION';
diff --git a/docs-core/src/test/java/com/sismics/util/TestGoogleAuthenticator.java b/docs-core/src/test/java/com/sismics/util/TestGoogleAuthenticator.java
new file mode 100644
index 00000000..37dddb00
--- /dev/null
+++ b/docs-core/src/test/java/com/sismics/util/TestGoogleAuthenticator.java
@@ -0,0 +1,26 @@
+package com.sismics.util;
+
+import java.util.Date;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.sismics.util.totp.GoogleAuthenticator;
+import com.sismics.util.totp.GoogleAuthenticatorKey;
+
+/**
+ * Test of {@link GoogleAuthenticator}
+ *
+ * @author bgamard
+ */
+public class TestGoogleAuthenticator {
+ @Test
+ public void testGoogleAuthenticator() {
+ GoogleAuthenticator gAuth = new GoogleAuthenticator();
+ GoogleAuthenticatorKey key = gAuth.createCredentials();
+ Assert.assertNotNull(key.getVerificationCode());
+ Assert.assertEquals(5, key.getScratchCodes().size());
+ int validationCode = gAuth.calculateCode(key.getKey(), new Date().getTime() / 30000);
+ Assert.assertTrue(gAuth.authorize(key.getKey(), validationCode));
+ }
+}
diff --git a/docs-parent/pom.xml b/docs-parent/pom.xml
index 3db35ad8..81362ea6 100644
--- a/docs-parent/pom.xml
+++ b/docs-parent/pom.xml
@@ -395,13 +395,15 @@
${com.twelvemonkeys.imageio.version}
-
+
+ com.levigo.jbig2levigo-jbig2-imageio${com.levigo.jbig2.levigo-jbig2-imageio.version}
-
+
+ com.github.jai-imageiojai-imageio-core${com.github.jai-imageio.jai-imageio-core.version}
diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java
index 4c476265..968f9adb 100644
--- a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java
+++ b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java
@@ -95,12 +95,11 @@ public class ClientUtil {
* @return Authentication token
*/
public String login(String username, String password, Boolean remember) {
- Form form = new Form();
- form.param("username", username);
- form.param("password", password);
- form.param("remember", remember.toString());
Response response = resource.path("/user/login").request()
- .post(Entity.form(form));
+ .post(Entity.form(new Form()
+ .param("username", username)
+ .param("password", password)
+ .param("remember", remember.toString())));
return getAuthenticationCookie(response);
}
diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties
index 44ddb414..04b5153a 100644
--- a/docs-web/src/dev/resources/config.properties
+++ b/docs-web/src/dev/resources/config.properties
@@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
-db.version=8
\ No newline at end of file
+db.version=9
\ No newline at end of file
diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java
index d27ca7c6..1c12c419 100644
--- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java
+++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java
@@ -55,6 +55,8 @@ import com.sismics.rest.util.JsonUtil;
import com.sismics.rest.util.ValidationUtil;
import com.sismics.security.UserPrincipal;
import com.sismics.util.filter.TokenBasedSecurityFilter;
+import com.sismics.util.totp.GoogleAuthenticator;
+import com.sismics.util.totp.GoogleAuthenticatorKey;
/**
* User REST resources.
@@ -253,6 +255,7 @@ public class UserResource extends BaseResource {
public Response login(
@FormParam("username") String username,
@FormParam("password") String password,
+ @FormParam("code") String validationCodeStr,
@FormParam("remember") boolean longLasted) {
// Validate the input data
username = StringUtils.strip(username);
@@ -260,10 +263,25 @@ public class UserResource extends BaseResource {
// Get the user
UserDao userDao = new UserDao();
- String userId = userDao.authenticate(username, password);
- if (userId == null) {
+ User user = userDao.authenticate(username, password);
+ if (user == null) {
throw new ForbiddenClientException();
}
+
+ // Two factor authentication
+ if (user.getTotpKey() != null) {
+ // If TOTP is enabled, ask a validation code
+ if (Strings.isNullOrEmpty(validationCodeStr)) {
+ throw new ClientException("ValidationCodeRequired", "An OTP validation code is required");
+ }
+
+ // Check the validation code
+ int validationCode = ValidationUtil.validateInteger(validationCodeStr, "code");
+ GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
+ if (!googleAuthenticator.authorize(user.getTotpKey(), validationCode)) {
+ throw new ForbiddenClientException();
+ }
+ }
// Get the remote IP
String ip = request.getHeader("x-forwarded-for");
@@ -273,15 +291,15 @@ public class UserResource extends BaseResource {
// Create a new session token
AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao();
- AuthenticationToken authenticationToken = new AuthenticationToken();
- authenticationToken.setUserId(userId);
- authenticationToken.setLongLasted(longLasted);
- authenticationToken.setIp(ip);
- authenticationToken.setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000));
+ AuthenticationToken authenticationToken = new AuthenticationToken()
+ .setUserId(user.getId())
+ .setLongLasted(longLasted)
+ .setIp(ip)
+ .setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000));
String token = authenticationTokenDao.create(authenticationToken);
// Cleanup old session tokens
- authenticationTokenDao.deleteOldSessionToken(userId);
+ authenticationTokenDao.deleteOldSessionToken(user.getId());
JsonObjectBuilder response = Json.createObjectBuilder();
int maxAge = longLasted ? TokenBasedSecurityFilter.TOKEN_LONG_LIFETIME : -1;
@@ -470,7 +488,8 @@ public class UserResource extends BaseResource {
response.add("username", user.getUsername())
.add("email", user.getEmail())
.add("storage_quota", user.getStorageQuota())
- .add("storage_current", user.getStorageCurrent());
+ .add("storage_current", user.getStorageCurrent())
+ .add("totp_enabled", user.getTotpKey() != null);
// Base functions
JsonArrayBuilder baseFunctions = Json.createArrayBuilder();
@@ -639,6 +658,66 @@ public class UserResource extends BaseResource {
return Response.ok().entity(response.build()).build();
}
+ /**
+ * Enable time-based one-time password.
+ *
+ * @return Response
+ */
+ @POST
+ @Path("enable_totp")
+ public Response enableTotp() {
+ if (!authenticate()) {
+ throw new ForbiddenClientException();
+ }
+
+ // Create a new TOTP key
+ GoogleAuthenticator gAuth = new GoogleAuthenticator();
+ final GoogleAuthenticatorKey key = gAuth.createCredentials();
+
+ // Save it
+ UserDao userDao = new UserDao();
+ User user = userDao.getActiveByUsername(principal.getName());
+ user.setTotpKey(key.getKey());
+ user = userDao.update(user, principal.getId());
+
+ JsonObjectBuilder response = Json.createObjectBuilder()
+ .add("secret", key.getKey());
+ return Response.ok().entity(response.build()).build();
+ }
+
+ /**
+ * Disable time-based one-time password.
+ *
+ * @param password Password
+ * @return Response
+ */
+ @POST
+ @Path("disable_totp")
+ public Response disableTotp(@FormParam("password") String password) {
+ if (!authenticate()) {
+ throw new ForbiddenClientException();
+ }
+
+ // Validate the input data
+ password = ValidationUtil.validateLength(password, "password", 1, 100, false);
+
+ // Check the password and get the user
+ UserDao userDao = new UserDao();
+ User user = userDao.authenticate(principal.getName(), password);
+ if (user == null) {
+ throw new ForbiddenClientException();
+ }
+
+ // Remove the TOTP key
+ user.setTotpKey(null);
+ userDao.update(user, principal.getId());
+
+ // Always return OK
+ JsonObjectBuilder response = Json.createObjectBuilder()
+ .add("status", "ok");
+ return Response.ok().entity(response.build()).build();
+ }
+
/**
* Returns the authentication token value.
*
diff --git a/docs-web/src/main/webapp/Gruntfile.js b/docs-web/src/main/webapp/Gruntfile.js
index 44adc424..cb4ad357 100644
--- a/docs-web/src/main/webapp/Gruntfile.js
+++ b/docs-web/src/main/webapp/Gruntfile.js
@@ -22,7 +22,7 @@ module.exports = function(grunt) {
separator: ';'
},
src: ['src/lib/jquery.js','src/lib/jquery.ui.js','src/lib/underscore.js','src/lib/colorpicker.js', 'src/lib/angular.js', 'src/lib/angular.*.js',
- 'dist/app/docs/app.js', 'dist/app/docs/controller/*.js', 'dist/app/docs/directive/*.js', 'dist/app/docs/filter/*.js', 'dist/app/docs/service/*.js'],
+ 'dist/app/docs/app.js', 'dist/app/docs/controller/**/*.js', 'dist/app/docs/directive/*.js', 'dist/app/docs/filter/*.js', 'dist/app/docs/service/*.js'],
dest: 'dist/docs.js'
},
share: {
diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js
index cdcfb0d1..1e7cea18 100644
--- a/docs-web/src/main/webapp/src/app/docs/app.js
+++ b/docs-web/src/main/webapp/src/app/docs/app.js
@@ -5,7 +5,7 @@
*/
angular.module('docs',
// Dependencies
- ['ui.router', 'ui.route', 'ui.bootstrap', 'ui.keypress', 'ui.validate', 'dialog',
+ ['ui.router', 'ui.route', 'ui.bootstrap', 'ui.keypress', 'ui.validate', 'dialog', 'ngProgress', 'monospaced.qrcode',
'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'angularFileUpload']
)
@@ -61,6 +61,15 @@ angular.module('docs',
}
}
})
+ .state('settings.security', {
+ url: '/security',
+ views: {
+ 'settings': {
+ templateUrl: 'partial/docs/settings.security.html',
+ controller: 'SettingsSecurity'
+ }
+ }
+ })
.state('settings.session', {
url: '/session',
views: {
@@ -345,4 +354,21 @@ angular.module('docs',
$state.go(redirect, toParams);
}
});
+})
+/**
+ * Initialize ngProgress.
+ */
+.run(function($rootScope, ngProgressFactory, $http) {
+ $rootScope.ngProgress = ngProgressFactory.createInstance();
+
+ // Watch for the number of XHR running
+ $rootScope.$watch(function() {
+ return $http.pendingRequests.length > 0
+ }, function(count) {
+ if (count == 0) {
+ $rootScope.ngProgress.complete();
+ } else {
+ $rootScope.ngProgress.start();
+ }
+ });
});
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Login.js b/docs-web/src/main/webapp/src/app/docs/controller/Login.js
index 9c695aa3..9a5fb7ce 100644
--- a/docs-web/src/main/webapp/src/app/docs/controller/Login.js
+++ b/docs-web/src/main/webapp/src/app/docs/controller/Login.js
@@ -4,18 +4,28 @@
* Login controller.
*/
angular.module('docs').controller('Login', function($scope, $rootScope, $state, $dialog, User) {
+ $scope.codeRequired = false;
+
+ /**
+ * Login.
+ */
$scope.login = function() {
User.login($scope.user).then(function() {
User.userInfo(true).then(function(data) {
$rootScope.userInfo = data;
});
$state.go('document.default');
- }, function() {
- var title = 'Login failed';
- var msg = 'Username or password invalid';
- var btns = [{result:'ok', label: 'OK', cssClass: 'btn-primary'}];
-
- $dialog.messageBox(title, msg, btns);
+ }, function(data) {
+ if (data.data.type == 'ValidationCodeRequired') {
+ // A TOTP validation code is required to login
+ $scope.codeRequired = true;
+ } else {
+ // Login truly failed
+ var title = 'Login failed';
+ var msg = 'Username or password invalid';
+ var btns = [{result: 'ok', label: 'OK', cssClass: 'btn-primary'}];
+ $dialog.messageBox(title, msg, btns);
+ }
});
};
});
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js b/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js
index d21bbb09..adbef67b 100644
--- a/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js
+++ b/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js
@@ -56,11 +56,4 @@ angular.module('docs').controller('Navigation', function($scope, $http, $state,
});
$event.preventDefault();
};
-
- /**
- * Returns true if at least an asynchronous request is in progress.
- */
- $scope.isLoading = function() {
- return $http.pendingRequests.length > 0;
- };
});
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Document.js b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/Document.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/Document.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/DocumentDefault.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/DocumentEdit.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentModalPdf.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentModalPdf.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/DocumentModalPdf.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/DocumentModalPdf.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentModalShare.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentModalShare.js
similarity index 84%
rename from docs-web/src/main/webapp/src/app/docs/controller/DocumentModalShare.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/DocumentModalShare.js
index 81bff01f..2695591d 100644
--- a/docs-web/src/main/webapp/src/app/docs/controller/DocumentModalShare.js
+++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentModalShare.js
@@ -5,7 +5,7 @@
*/
angular.module('docs').controller('DocumentModalShare', function ($scope, $modalInstance) {
$scope.name = '';
- $scope.close = function (name) {
+ $scope.close = function(name) {
$modalInstance.close(name);
}
});
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/DocumentView.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentViewActivity.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewActivity.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/DocumentViewActivity.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewActivity.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentViewContent.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/DocumentViewContent.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/DocumentViewPermissions.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewPermissions.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/DocumentViewPermissions.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewPermissions.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/FileModalView.js b/docs-web/src/main/webapp/src/app/docs/controller/document/FileModalView.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/FileModalView.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/FileModalView.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/FileView.js b/docs-web/src/main/webapp/src/app/docs/controller/document/FileView.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/FileView.js
rename to docs-web/src/main/webapp/src/app/docs/controller/document/FileView.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Settings.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/Settings.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/Settings.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/Settings.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsAccount.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsAccount.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/SettingsAccount.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsAccount.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsDefault.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsDefault.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/SettingsDefault.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsDefault.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsGroup.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsGroup.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/SettingsGroup.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsGroup.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsGroupEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsGroupEdit.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/SettingsGroupEdit.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsGroupEdit.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsLog.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsLog.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/SettingsLog.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsLog.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js
new file mode 100644
index 00000000..76037eb5
--- /dev/null
+++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurity.js
@@ -0,0 +1,53 @@
+'use strict';
+
+/**
+ * Settings security controller.
+ */
+angular.module('docs').controller('SettingsSecurity', function($scope, User, $dialog, $modal, Restangular) {
+ User.userInfo().then(function(data) {
+ $scope.user = data;
+ });
+
+ /**
+ * Enable TOTP.
+ */
+ $scope.enableTotp = function() {
+ var title = 'Enable two-factor authentication';
+ var msg = 'Make sure you have a TOTP-compatible application on your phone ready to add a new account';
+ var btns = [{result:'cancel', label: 'Cancel'}, {result:'ok', label: 'OK', cssClass: 'btn-primary'}];
+
+ $dialog.messageBox(title, msg, btns, function(result) {
+ if (result == 'ok') {
+ Restangular.one('user/enable_totp').post().then(function(data) {
+ $scope.secret = data.secret;
+ User.userInfo(true).then(function(data) {
+ $scope.user = data;
+ })
+ });
+ }
+ });
+ };
+
+ /**
+ * Disable TOTP.
+ */
+ $scope.disableTotp = function() {
+ $modal.open({
+ templateUrl: 'partial/docs/settings.security.disabletotp.html',
+ controller: 'SettingsSecurityModalDisableTotp'
+ }).result.then(function (password) {
+ if (password == null) {
+ return;
+ }
+
+ // Disable TOTP
+ Restangular.one('user/disable_totp').post('', {
+ password: password
+ }).then(function() {
+ User.userInfo(true).then(function(data) {
+ $scope.user = data;
+ })
+ });
+ });
+ };
+});
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurityModalDisableTotp.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurityModalDisableTotp.js
new file mode 100644
index 00000000..2f588bdb
--- /dev/null
+++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSecurityModalDisableTotp.js
@@ -0,0 +1,11 @@
+'use strict';
+
+/**
+ * Settings modal disable TOTP controller.
+ */
+angular.module('docs').controller('SettingsSecurityModalDisableTotp', function ($scope, $modalInstance) {
+ $scope.password = '';
+ $scope.close = function(password) {
+ $modalInstance.close(password);
+ }
+});
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsSession.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSession.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/SettingsSession.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsSession.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsUser.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUser.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/SettingsUser.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUser.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsUserEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/SettingsUserEdit.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsUserEdit.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/SettingsVocabulary.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsVocabulary.js
similarity index 90%
rename from docs-web/src/main/webapp/src/app/docs/controller/SettingsVocabulary.js
rename to docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsVocabulary.js
index e23cf437..38254843 100644
--- a/docs-web/src/main/webapp/src/app/docs/controller/SettingsVocabulary.js
+++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsVocabulary.js
@@ -34,8 +34,8 @@ angular.module('docs').controller('SettingsVocabulary', function($scope, Restang
// Add an entry
$scope.addEntry = function(entry) {
entry.name = $scope.vocabulary;
- Restangular.one('vocabulary').put(entry).then(function() {
- $scope.entries.push(entry);
+ Restangular.one('vocabulary').put(entry).then(function(data) {
+ $scope.entries.push(data);
$scope.entry = {};
});
};
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Tag.js b/docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/Tag.js
rename to docs-web/src/main/webapp/src/app/docs/controller/tag/Tag.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/GroupProfile.js b/docs-web/src/main/webapp/src/app/docs/controller/usergroup/GroupProfile.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/GroupProfile.js
rename to docs-web/src/main/webapp/src/app/docs/controller/usergroup/GroupProfile.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/UserGroup.js b/docs-web/src/main/webapp/src/app/docs/controller/usergroup/UserGroup.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/UserGroup.js
rename to docs-web/src/main/webapp/src/app/docs/controller/usergroup/UserGroup.js
diff --git a/docs-web/src/main/webapp/src/app/docs/controller/UserProfile.js b/docs-web/src/main/webapp/src/app/docs/controller/usergroup/UserProfile.js
similarity index 100%
rename from docs-web/src/main/webapp/src/app/docs/controller/UserProfile.js
rename to docs-web/src/main/webapp/src/app/docs/controller/usergroup/UserProfile.js
diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html
index 384c59b1..7772ef24 100644
--- a/docs-web/src/main/webapp/src/index.html
+++ b/docs-web/src/main/webapp/src/index.html
@@ -37,35 +37,39 @@
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -92,10 +96,6 @@
-
-
-
-
diff --git a/docs-web/src/main/webapp/src/lib/angular.ngprogress.js b/docs-web/src/main/webapp/src/lib/angular.ngprogress.js
new file mode 100644
index 00000000..52bafac6
--- /dev/null
+++ b/docs-web/src/main/webapp/src/lib/angular.ngprogress.js
@@ -0,0 +1,220 @@
+/*
+ ngprogress 1.1.2 - slim, site-wide progressbar for AngularJS
+ (C) 2013 - Victor Bjelkholm
+ License: MIT
+ Source: https://github.com/VictorBjelkholm/ngProgress
+ Date Compiled: 2015-07-27
+ */
+angular.module('ngProgress.provider', ['ngProgress.directive'])
+ .service('ngProgress', function () {
+ 'use strict';
+ return ['$document', '$window', '$compile', '$rootScope', '$timeout', function($document, $window, $compile, $rootScope, $timeout) {
+ this.autoStyle = true;
+ this.count = 0;
+ this.height = '2px';
+ this.$scope = $rootScope.$new();
+ this.color = 'white';
+ this.parent = $document.find('body')[0];
+ this.count = 0;
+
+ // Compile the directive
+ this.progressbarEl = $compile('')(this.$scope);
+ // Add the element to body
+ this.parent.appendChild(this.progressbarEl[0]);
+ // Set the initial height
+ this.$scope.count = this.count;
+ // If height or color isn't undefined, set the height, background-color and color.
+ if (this.height !== undefined) {
+ this.progressbarEl.eq(0).children().css('height', this.height);
+ }
+ if (this.color !== undefined) {
+ this.progressbarEl.eq(0).children().css('background-color', this.color);
+ this.progressbarEl.eq(0).children().css('color', this.color);
+ }
+ // The ID for the interval controlling start()
+ this.intervalCounterId = 0;
+
+ // Starts the animation and adds between 0 - 5 percent to loading
+ // each 400 milliseconds. Should always be finished with progressbar.complete()
+ // to hide it
+ this.start = function () {
+ // TODO Use requestAnimationFrame instead of setInterval
+ // https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame
+ this.show();
+ var self = this;
+ clearInterval(this.intervalCounterId);
+ this.intervalCounterId = setInterval(function () {
+ if (isNaN(self.count)) {
+ clearInterval(self.intervalCounterId);
+ self.count = 0;
+ self.hide();
+ } else {
+ self.remaining = 100 - self.count;
+ self.count = self.count + (0.15 * Math.pow(1 - Math.sqrt(self.remaining), 2));
+ self.updateCount(self.count);
+ }
+ }, 200);
+ };
+ this.updateCount = function (new_count) {
+ this.$scope.count = new_count;
+ if(!this.$scope.$$phase) {
+ this.$scope.$apply();
+ }
+ };
+ // Sets the height of the progressbar. Use any valid CSS value
+ // Eg '10px', '1em' or '1%'
+ this.setHeight = function (new_height) {
+ if (new_height !== undefined) {
+ this.height = new_height;
+ this.$scope.height = this.height;
+ if(!this.$scope.$$phase) {
+ this.$scope.$apply();
+ }
+ }
+ return this.height;
+ };
+ // Sets the color of the progressbar and it's shadow. Use any valid HTML
+ // color
+ this.setColor = function(new_color) {
+ if (new_color !== undefined) {
+ this.color = new_color;
+ this.$scope.color = this.color;
+ if(!this.$scope.$$phase) {
+ this.$scope.$apply();
+ }
+ }
+ return this.color;
+ };
+ this.hide = function() {
+ this.progressbarEl.children().css('opacity', '0');
+ var self = this;
+ self.animate(function () {
+ self.progressbarEl.children().css('width', '0%');
+ self.animate(function () {
+ self.show();
+ }, 500);
+ }, 500);
+ };
+ this.show = function () {
+ var self = this;
+ self.animate(function () {
+ self.progressbarEl.children().css('opacity', '1');
+ }, 100);
+ };
+ // Cancel any prior animations before running new ones.
+ // Multiple simultaneous animations just look weird.
+ this.animate = function(fn, time) {
+ if(this.animation !== undefined) { $timeout.cancel(this.animation); }
+ this.animation = $timeout(fn, time);
+ };
+ // Returns on how many percent the progressbar is at. Should'nt be needed
+ this.status = function () {
+ return this.count;
+ };
+ // Stops the progressbar at it's current location
+ this.stop = function () {
+ clearInterval(this.intervalCounterId);
+ };
+ // Set's the progressbar percentage. Use a number between 0 - 100.
+ // If 100 is provided, complete will be called.
+ this.set = function (new_count) {
+ this.show();
+ this.updateCount(new_count);
+ this.count = new_count;
+ clearInterval(this.intervalCounterId);
+ return this.count;
+ };
+ this.css = function (args) {
+ return this.progressbarEl.children().css(args);
+ };
+ // Resets the progressbar to percetage 0 and therefore will be hided after
+ // it's rollbacked
+ this.reset = function () {
+ clearInterval(this.intervalCounterId);
+ this.count = 0;
+ this.updateCount(this.count);
+ return 0;
+ };
+ // Jumps to 100% progress and fades away progressbar.
+ this.complete = function () {
+ this.count = 100;
+ this.updateCount(this.count);
+ var self = this;
+ clearInterval(this.intervalCounterId);
+ $timeout(function () {
+ self.hide();
+ $timeout(function () {
+ self.count = 0;
+ self.updateCount(self.count);
+ }, 200);
+ }, 500);
+ return this.count;
+ };
+ // Set the parent of the directive, sometimes body is not sufficient
+ this.setParent = function(newParent) {
+ if(newParent === null || newParent === undefined) {
+ throw new Error('Provide a valid parent of type HTMLElement');
+ }
+
+ if(this.parent !== null && this.parent !== undefined) {
+ this.parent.removeChild(this.progressbarEl[0]);
+ }
+
+ this.parent = newParent;
+ this.parent.appendChild(this.progressbarEl[0]);
+ };
+ // Gets the current element the progressbar is attached to
+ this.getDomElement = function () {
+ return this.progressbarEl;
+ };
+ this.setAbsolute = function() {
+ this.progressbarEl.css('position', 'absolute');
+ };
+ }];
+ })
+ .factory('ngProgressFactory', ['$injector', 'ngProgress', function($injector, ngProgress) {
+ var service = {
+ createInstance: function () {
+ return $injector.instantiate(ngProgress);
+ }
+ };
+ return service;
+ }]);
+angular.module('ngProgress.directive', [])
+ .directive('ngProgress', ["$window", "$rootScope", function ($window, $rootScope) {
+ var directiveObj = {
+ // Replace the directive
+ replace: true,
+ // Only use as a element
+ restrict: 'E',
+ link: function ($scope, $element, $attrs, $controller) {
+ // Watch the count on the $rootScope. As soon as count changes to something that
+ // isn't undefined or null, change the counter on $scope and also the width of
+ // the progressbar. The same goes for color and height on the $rootScope
+ $scope.$watch('count', function (newVal) {
+ if (newVal !== undefined || newVal !== null) {
+ $scope.counter = newVal;
+ $element.eq(0).children().css('width', newVal + '%');
+ }
+ });
+ $scope.$watch('color', function (newVal) {
+ if (newVal !== undefined || newVal !== null) {
+ $scope.color = newVal;
+ $element.eq(0).children().css('background-color', newVal);
+ $element.eq(0).children().css('color', newVal);
+ }
+ });
+ $scope.$watch('height', function (newVal) {
+ if (newVal !== undefined || newVal !== null) {
+ $scope.height = newVal;
+ $element.eq(0).children().css('height', newVal);
+ }
+ });
+ },
+ // The actual html that will be used
+ template: '
'
+ };
+ return directiveObj;
+ }]);
+
+angular.module('ngProgress', ['ngProgress.directive', 'ngProgress.provider']);
\ No newline at end of file
diff --git a/docs-web/src/main/webapp/src/lib/angular.qrcode.js b/docs-web/src/main/webapp/src/lib/angular.qrcode.js
new file mode 100644
index 00000000..82a087a3
--- /dev/null
+++ b/docs-web/src/main/webapp/src/lib/angular.qrcode.js
@@ -0,0 +1,2015 @@
+//---------------------------------------------------------------------
+//
+// QR Code Generator for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+// http://www.opensource.org/licenses/mit-license.php
+//
+// The word 'QR Code' is registered trademark of
+// DENSO WAVE INCORPORATED
+// http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+
+var qrcode = function() {
+
+ //---------------------------------------------------------------------
+ // qrcode
+ //---------------------------------------------------------------------
+
+ /**
+ * qrcode
+ * @param typeNumber 1 to 40
+ * @param errorCorrectLevel 'L','M','Q','H'
+ */
+ var qrcode = function(typeNumber, errorCorrectLevel) {
+
+ var PAD0 = 0xEC;
+ var PAD1 = 0x11;
+
+ var _typeNumber = typeNumber;
+ var _errorCorrectLevel = QRErrorCorrectLevel[errorCorrectLevel];
+ var _modules = null;
+ var _moduleCount = 0;
+ var _dataCache = null;
+ var _dataList = new Array();
+
+ var _this = {};
+
+ var makeImpl = function(test, maskPattern) {
+
+ _moduleCount = _typeNumber * 4 + 17;
+ _modules = function(moduleCount) {
+ var modules = new Array(moduleCount);
+ for (var row = 0; row < moduleCount; row += 1) {
+ modules[row] = new Array(moduleCount);
+ for (var col = 0; col < moduleCount; col += 1) {
+ modules[row][col] = null;
+ }
+ }
+ return modules;
+ }(_moduleCount);
+
+ setupPositionProbePattern(0, 0);
+ setupPositionProbePattern(_moduleCount - 7, 0);
+ setupPositionProbePattern(0, _moduleCount - 7);
+ setupPositionAdjustPattern();
+ setupTimingPattern();
+ setupTypeInfo(test, maskPattern);
+
+ if (_typeNumber >= 7) {
+ setupTypeNumber(test);
+ }
+
+ if (_dataCache == null) {
+ _dataCache = createData(_typeNumber, _errorCorrectLevel, _dataList);
+ }
+
+ mapData(_dataCache, maskPattern);
+ };
+
+ var setupPositionProbePattern = function(row, col) {
+
+ for (var r = -1; r <= 7; r += 1) {
+
+ if (row + r <= -1 || _moduleCount <= row + r) continue;
+
+ for (var c = -1; c <= 7; c += 1) {
+
+ if (col + c <= -1 || _moduleCount <= col + c) continue;
+
+ if ( (0 <= r && r <= 6 && (c == 0 || c == 6) )
+ || (0 <= c && c <= 6 && (r == 0 || r == 6) )
+ || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ };
+
+ var getBestMaskPattern = function() {
+
+ var minLostPoint = 0;
+ var pattern = 0;
+
+ for (var i = 0; i < 8; i += 1) {
+
+ makeImpl(true, i);
+
+ var lostPoint = QRUtil.getLostPoint(_this);
+
+ if (i == 0 || minLostPoint > lostPoint) {
+ minLostPoint = lostPoint;
+ pattern = i;
+ }
+ }
+
+ return pattern;
+ };
+
+ var setupTimingPattern = function() {
+
+ for (var r = 8; r < _moduleCount - 8; r += 1) {
+ if (_modules[r][6] != null) {
+ continue;
+ }
+ _modules[r][6] = (r % 2 == 0);
+ }
+
+ for (var c = 8; c < _moduleCount - 8; c += 1) {
+ if (_modules[6][c] != null) {
+ continue;
+ }
+ _modules[6][c] = (c % 2 == 0);
+ }
+ };
+
+ var setupPositionAdjustPattern = function() {
+
+ var pos = QRUtil.getPatternPosition(_typeNumber);
+
+ for (var i = 0; i < pos.length; i += 1) {
+
+ for (var j = 0; j < pos.length; j += 1) {
+
+ var row = pos[i];
+ var col = pos[j];
+
+ if (_modules[row][col] != null) {
+ continue;
+ }
+
+ for (var r = -2; r <= 2; r += 1) {
+
+ for (var c = -2; c <= 2; c += 1) {
+
+ if (r == -2 || r == 2 || c == -2 || c == 2
+ || (r == 0 && c == 0) ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ }
+ }
+ };
+
+ var setupTypeNumber = function(test) {
+
+ var bits = QRUtil.getBCHTypeNumber(_typeNumber);
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+ _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;
+ }
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+ _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
+ }
+ };
+
+ var setupTypeInfo = function(test, maskPattern) {
+
+ var data = (_errorCorrectLevel << 3) | maskPattern;
+ var bits = QRUtil.getBCHTypeInfo(data);
+
+ // vertical
+ for (var i = 0; i < 15; i += 1) {
+
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+
+ if (i < 6) {
+ _modules[i][8] = mod;
+ } else if (i < 8) {
+ _modules[i + 1][8] = mod;
+ } else {
+ _modules[_moduleCount - 15 + i][8] = mod;
+ }
+ }
+
+ // horizontal
+ for (var i = 0; i < 15; i += 1) {
+
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+
+ if (i < 8) {
+ _modules[8][_moduleCount - i - 1] = mod;
+ } else if (i < 9) {
+ _modules[8][15 - i - 1 + 1] = mod;
+ } else {
+ _modules[8][15 - i - 1] = mod;
+ }
+ }
+
+ // fixed module
+ _modules[_moduleCount - 8][8] = (!test);
+ };
+
+ var mapData = function(data, maskPattern) {
+
+ var inc = -1;
+ var row = _moduleCount - 1;
+ var bitIndex = 7;
+ var byteIndex = 0;
+ var maskFunc = QRUtil.getMaskFunction(maskPattern);
+
+ for (var col = _moduleCount - 1; col > 0; col -= 2) {
+
+ if (col == 6) col -= 1;
+
+ while (true) {
+
+ for (var c = 0; c < 2; c += 1) {
+
+ if (_modules[row][col - c] == null) {
+
+ var dark = false;
+
+ if (byteIndex < data.length) {
+ dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1);
+ }
+
+ var mask = maskFunc(row, col - c);
+
+ if (mask) {
+ dark = !dark;
+ }
+
+ _modules[row][col - c] = dark;
+ bitIndex -= 1;
+
+ if (bitIndex == -1) {
+ byteIndex += 1;
+ bitIndex = 7;
+ }
+ }
+ }
+
+ row += inc;
+
+ if (row < 0 || _moduleCount <= row) {
+ row -= inc;
+ inc = -inc;
+ break;
+ }
+ }
+ }
+ };
+
+ var createBytes = function(buffer, rsBlocks) {
+
+ var offset = 0;
+
+ var maxDcCount = 0;
+ var maxEcCount = 0;
+
+ var dcdata = new Array(rsBlocks.length);
+ var ecdata = new Array(rsBlocks.length);
+
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+
+ var dcCount = rsBlocks[r].dataCount;
+ var ecCount = rsBlocks[r].totalCount - dcCount;
+
+ maxDcCount = Math.max(maxDcCount, dcCount);
+ maxEcCount = Math.max(maxEcCount, ecCount);
+
+ dcdata[r] = new Array(dcCount);
+
+ for (var i = 0; i < dcdata[r].length; i += 1) {
+ dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
+ }
+ offset += dcCount;
+
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
+ var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
+
+ var modPoly = rawPoly.mod(rsPoly);
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
+ for (var i = 0; i < ecdata[r].length; i += 1) {
+ var modIndex = i + modPoly.getLength() - ecdata[r].length;
+ ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0;
+ }
+ }
+
+ var totalCodeCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalCodeCount += rsBlocks[i].totalCount;
+ }
+
+ var data = new Array(totalCodeCount);
+ var index = 0;
+
+ for (var i = 0; i < maxDcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < dcdata[r].length) {
+ data[index] = dcdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ for (var i = 0; i < maxEcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < ecdata[r].length) {
+ data[index] = ecdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ return data;
+ };
+
+ var createData = function(typeNumber, errorCorrectLevel, dataList) {
+
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
+
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < dataList.length; i += 1) {
+ var data = dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
+ data.write(buffer);
+ }
+
+ // calc num max data.
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() > totalDataCount * 8) {
+ throw new Error('code length overflow. ('
+ + buffer.getLengthInBits()
+ + '>'
+ + totalDataCount * 8
+ + ')');
+ }
+
+ // end code
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+ buffer.put(0, 4);
+ }
+
+ // padding
+ while (buffer.getLengthInBits() % 8 != 0) {
+ buffer.putBit(false);
+ }
+
+ // padding
+ while (true) {
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD0, 8);
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD1, 8);
+ }
+
+ return createBytes(buffer, rsBlocks);
+ };
+
+ _this.addData = function(data) {
+ var newData = qr8BitByte(data);
+ _dataList.push(newData);
+ _dataCache = null;
+ };
+
+ _this.isDark = function(row, col) {
+ if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
+ throw new Error(row + ',' + col);
+ }
+ return _modules[row][col];
+ };
+
+ _this.getModuleCount = function() {
+ return _moduleCount;
+ };
+
+ _this.make = function() {
+ makeImpl(false, getBestMaskPattern() );
+ };
+
+ _this.createTableTag = function(cellSize, margin) {
+
+ cellSize = cellSize || 2;
+ margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+ var qrHtml = '';
+
+ qrHtml += '
';
+ qrHtml += '';
+
+ for (var r = 0; r < _this.getModuleCount(); r += 1) {
+
+ qrHtml += '
';
+
+ for (var c = 0; c < _this.getModuleCount(); c += 1) {
+ qrHtml += '