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.lucene lucene-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: + *

+ *

+ * 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.jbig2 levigo-jbig2-imageio ${com.levigo.jbig2.levigo-jbig2-imageio.version} - + + com.github.jai-imageio jai-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 += ''; + } + + qrHtml += ''; + qrHtml += '
'; + } + + qrHtml += '
'; + + return qrHtml; + }; + + _this.createImgTag = function(cellSize, margin) { + + cellSize = cellSize || 2; + margin = (typeof margin == 'undefined')? cellSize * 4 : margin; + + var size = _this.getModuleCount() * cellSize + margin * 2; + var min = margin; + var max = size - margin; + + return createImgTag(size, size, function(x, y) { + if (min <= x && x < max && min <= y && y < max) { + var c = Math.floor( (x - min) / cellSize); + var r = Math.floor( (y - min) / cellSize); + return _this.isDark(r, c)? 0 : 1; + } else { + return 1; + } + } ); + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qrcode.stringToBytes + //--------------------------------------------------------------------- + + qrcode.stringToBytes = function(s) { + var bytes = new Array(); + for (var i = 0; i < s.length; i += 1) { + var c = s.charCodeAt(i); + bytes.push(c & 0xff); + } + return bytes; + }; + + //--------------------------------------------------------------------- + // qrcode.createStringToBytes + //--------------------------------------------------------------------- + + /** + * @param unicodeData base64 string of byte array. + * [16bit Unicode],[16bit Bytes], ... + * @param numChars + */ + qrcode.createStringToBytes = function(unicodeData, numChars) { + + // create conversion map. + + var unicodeMap = function() { + + var bin = base64DecodeInputStream(unicodeData); + var read = function() { + var b = bin.read(); + if (b == -1) throw new Error(); + return b; + }; + + var count = 0; + var unicodeMap = {}; + while (true) { + var b0 = bin.read(); + if (b0 == -1) break; + var b1 = read(); + var b2 = read(); + var b3 = read(); + var k = String.fromCharCode( (b0 << 8) | b1); + var v = (b2 << 8) | b3; + unicodeMap[k] = v; + count += 1; + } + if (count != numChars) { + throw new Error(count + ' != ' + numChars); + } + + return unicodeMap; + }(); + + var unknownChar = '?'.charCodeAt(0); + + return function(s) { + var bytes = new Array(); + for (var i = 0; i < s.length; i += 1) { + var c = s.charCodeAt(i); + if (c < 128) { + bytes.push(c); + } else { + var b = unicodeMap[s.charAt(i)]; + if (typeof b == 'number') { + if ( (b & 0xff) == b) { + // 1byte + bytes.push(b); + } else { + // 2bytes + bytes.push(b >>> 8); + bytes.push(b & 0xff); + } + } else { + bytes.push(unknownChar); + } + } + } + return bytes; + }; + }; + + //--------------------------------------------------------------------- + // QRMode + //--------------------------------------------------------------------- + + var QRMode = { + MODE_NUMBER : 1 << 0, + MODE_ALPHA_NUM : 1 << 1, + MODE_8BIT_BYTE : 1 << 2, + MODE_KANJI : 1 << 3 + }; + + //--------------------------------------------------------------------- + // QRErrorCorrectLevel + //--------------------------------------------------------------------- + + var QRErrorCorrectLevel = { + L : 1, + M : 0, + Q : 3, + H : 2 + }; + + //--------------------------------------------------------------------- + // QRMaskPattern + //--------------------------------------------------------------------- + + var QRMaskPattern = { + PATTERN000 : 0, + PATTERN001 : 1, + PATTERN010 : 2, + PATTERN011 : 3, + PATTERN100 : 4, + PATTERN101 : 5, + PATTERN110 : 6, + PATTERN111 : 7 + }; + + //--------------------------------------------------------------------- + // QRUtil + //--------------------------------------------------------------------- + + var QRUtil = function() { + + var PATTERN_POSITION_TABLE = [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ]; + var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0); + var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0); + var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1); + + var _this = {}; + + var getBCHDigit = function(data) { + var digit = 0; + while (data != 0) { + digit += 1; + data >>>= 1; + } + return digit; + }; + + _this.getBCHTypeInfo = function(data) { + var d = data << 10; + while (getBCHDigit(d) - getBCHDigit(G15) >= 0) { + d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) ); + } + return ( (data << 10) | d) ^ G15_MASK; + }; + + _this.getBCHTypeNumber = function(data) { + var d = data << 12; + while (getBCHDigit(d) - getBCHDigit(G18) >= 0) { + d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) ); + } + return (data << 12) | d; + }; + + _this.getPatternPosition = function(typeNumber) { + return PATTERN_POSITION_TABLE[typeNumber - 1]; + }; + + _this.getMaskFunction = function(maskPattern) { + + switch (maskPattern) { + + case QRMaskPattern.PATTERN000 : + return function(i, j) { return (i + j) % 2 == 0; }; + case QRMaskPattern.PATTERN001 : + return function(i, j) { return i % 2 == 0; }; + case QRMaskPattern.PATTERN010 : + return function(i, j) { return j % 3 == 0; }; + case QRMaskPattern.PATTERN011 : + return function(i, j) { return (i + j) % 3 == 0; }; + case QRMaskPattern.PATTERN100 : + return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; }; + case QRMaskPattern.PATTERN101 : + return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; }; + case QRMaskPattern.PATTERN110 : + return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; }; + case QRMaskPattern.PATTERN111 : + return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; }; + + default : + throw new Error('bad maskPattern:' + maskPattern); + } + }; + + _this.getErrorCorrectPolynomial = function(errorCorrectLength) { + var a = qrPolynomial([1], 0); + for (var i = 0; i < errorCorrectLength; i += 1) { + a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) ); + } + return a; + }; + + _this.getLengthInBits = function(mode, type) { + + if (1 <= type && type < 10) { + + // 1 - 9 + + switch(mode) { + case QRMode.MODE_NUMBER : return 10; + case QRMode.MODE_ALPHA_NUM : return 9; + case QRMode.MODE_8BIT_BYTE : return 8; + case QRMode.MODE_KANJI : return 8; + default : + throw new Error('mode:' + mode); + } + + } else if (type < 27) { + + // 10 - 26 + + switch(mode) { + case QRMode.MODE_NUMBER : return 12; + case QRMode.MODE_ALPHA_NUM : return 11; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 10; + default : + throw new Error('mode:' + mode); + } + + } else if (type < 41) { + + // 27 - 40 + + switch(mode) { + case QRMode.MODE_NUMBER : return 14; + case QRMode.MODE_ALPHA_NUM : return 13; + case QRMode.MODE_8BIT_BYTE : return 16; + case QRMode.MODE_KANJI : return 12; + default : + throw new Error('mode:' + mode); + } + + } else { + throw new Error('type:' + type); + } + }; + + _this.getLostPoint = function(qrcode) { + + var moduleCount = qrcode.getModuleCount(); + + var lostPoint = 0; + + // LEVEL1 + + for (var row = 0; row < moduleCount; row += 1) { + for (var col = 0; col < moduleCount; col += 1) { + + var sameCount = 0; + var dark = qrcode.isDark(row, col); + + for (var r = -1; r <= 1; r += 1) { + + if (row + r < 0 || moduleCount <= row + r) { + continue; + } + + for (var c = -1; c <= 1; c += 1) { + + if (col + c < 0 || moduleCount <= col + c) { + continue; + } + + if (r == 0 && c == 0) { + continue; + } + + if (dark == qrcode.isDark(row + r, col + c) ) { + sameCount += 1; + } + } + } + + if (sameCount > 5) { + lostPoint += (3 + sameCount - 5); + } + } + }; + + // LEVEL2 + + for (var row = 0; row < moduleCount - 1; row += 1) { + for (var col = 0; col < moduleCount - 1; col += 1) { + var count = 0; + if (qrcode.isDark(row, col) ) count += 1; + if (qrcode.isDark(row + 1, col) ) count += 1; + if (qrcode.isDark(row, col + 1) ) count += 1; + if (qrcode.isDark(row + 1, col + 1) ) count += 1; + if (count == 0 || count == 4) { + lostPoint += 3; + } + } + } + + // LEVEL3 + + for (var row = 0; row < moduleCount; row += 1) { + for (var col = 0; col < moduleCount - 6; col += 1) { + if (qrcode.isDark(row, col) + && !qrcode.isDark(row, col + 1) + && qrcode.isDark(row, col + 2) + && qrcode.isDark(row, col + 3) + && qrcode.isDark(row, col + 4) + && !qrcode.isDark(row, col + 5) + && qrcode.isDark(row, col + 6) ) { + lostPoint += 40; + } + } + } + + for (var col = 0; col < moduleCount; col += 1) { + for (var row = 0; row < moduleCount - 6; row += 1) { + if (qrcode.isDark(row, col) + && !qrcode.isDark(row + 1, col) + && qrcode.isDark(row + 2, col) + && qrcode.isDark(row + 3, col) + && qrcode.isDark(row + 4, col) + && !qrcode.isDark(row + 5, col) + && qrcode.isDark(row + 6, col) ) { + lostPoint += 40; + } + } + } + + // LEVEL4 + + var darkCount = 0; + + for (var col = 0; col < moduleCount; col += 1) { + for (var row = 0; row < moduleCount; row += 1) { + if (qrcode.isDark(row, col) ) { + darkCount += 1; + } + } + } + + var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; + lostPoint += ratio * 10; + + return lostPoint; + }; + + return _this; + }(); + + //--------------------------------------------------------------------- + // QRMath + //--------------------------------------------------------------------- + + var QRMath = function() { + + var EXP_TABLE = new Array(256); + var LOG_TABLE = new Array(256); + + // initialize tables + for (var i = 0; i < 8; i += 1) { + EXP_TABLE[i] = 1 << i; + } + for (var i = 8; i < 256; i += 1) { + EXP_TABLE[i] = EXP_TABLE[i - 4] + ^ EXP_TABLE[i - 5] + ^ EXP_TABLE[i - 6] + ^ EXP_TABLE[i - 8]; + } + for (var i = 0; i < 255; i += 1) { + LOG_TABLE[EXP_TABLE[i] ] = i; + } + + var _this = {}; + + _this.glog = function(n) { + + if (n < 1) { + throw new Error('glog(' + n + ')'); + } + + return LOG_TABLE[n]; + }; + + _this.gexp = function(n) { + + while (n < 0) { + n += 255; + } + + while (n >= 256) { + n -= 255; + } + + return EXP_TABLE[n]; + }; + + return _this; + }(); + + //--------------------------------------------------------------------- + // qrPolynomial + //--------------------------------------------------------------------- + + function qrPolynomial(num, shift) { + + if (typeof num.length == 'undefined') { + throw new Error(num.length + '/' + shift); + } + + var _num = function() { + var offset = 0; + while (offset < num.length && num[offset] == 0) { + offset += 1; + } + var _num = new Array(num.length - offset + shift); + for (var i = 0; i < num.length - offset; i += 1) { + _num[i] = num[i + offset]; + } + return _num; + }(); + + var _this = {}; + + _this.getAt = function(index) { + return _num[index]; + }; + + _this.getLength = function() { + return _num.length; + }; + + _this.multiply = function(e) { + + var num = new Array(_this.getLength() + e.getLength() - 1); + + for (var i = 0; i < _this.getLength(); i += 1) { + for (var j = 0; j < e.getLength(); j += 1) { + num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i) ) + QRMath.glog(e.getAt(j) ) ); + } + } + + return qrPolynomial(num, 0); + }; + + _this.mod = function(e) { + + if (_this.getLength() - e.getLength() < 0) { + return _this; + } + + var ratio = QRMath.glog(_this.getAt(0) ) - QRMath.glog(e.getAt(0) ); + + var num = new Array(_this.getLength() ); + for (var i = 0; i < _this.getLength(); i += 1) { + num[i] = _this.getAt(i); + } + + for (var i = 0; i < e.getLength(); i += 1) { + num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i) ) + ratio); + } + + // recursive call + return qrPolynomial(num, 0).mod(e); + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // QRRSBlock + //--------------------------------------------------------------------- + + var QRRSBlock = function() { + + var RS_BLOCK_TABLE = [ + + // L + // M + // Q + // H + + // 1 + [1, 26, 19], + [1, 26, 16], + [1, 26, 13], + [1, 26, 9], + + // 2 + [1, 44, 34], + [1, 44, 28], + [1, 44, 22], + [1, 44, 16], + + // 3 + [1, 70, 55], + [1, 70, 44], + [2, 35, 17], + [2, 35, 13], + + // 4 + [1, 100, 80], + [2, 50, 32], + [2, 50, 24], + [4, 25, 9], + + // 5 + [1, 134, 108], + [2, 67, 43], + [2, 33, 15, 2, 34, 16], + [2, 33, 11, 2, 34, 12], + + // 6 + [2, 86, 68], + [4, 43, 27], + [4, 43, 19], + [4, 43, 15], + + // 7 + [2, 98, 78], + [4, 49, 31], + [2, 32, 14, 4, 33, 15], + [4, 39, 13, 1, 40, 14], + + // 8 + [2, 121, 97], + [2, 60, 38, 2, 61, 39], + [4, 40, 18, 2, 41, 19], + [4, 40, 14, 2, 41, 15], + + // 9 + [2, 146, 116], + [3, 58, 36, 2, 59, 37], + [4, 36, 16, 4, 37, 17], + [4, 36, 12, 4, 37, 13], + + // 10 + [2, 86, 68, 2, 87, 69], + [4, 69, 43, 1, 70, 44], + [6, 43, 19, 2, 44, 20], + [6, 43, 15, 2, 44, 16], + + // 11 + [4, 101, 81], + [1, 80, 50, 4, 81, 51], + [4, 50, 22, 4, 51, 23], + [3, 36, 12, 8, 37, 13], + + // 12 + [2, 116, 92, 2, 117, 93], + [6, 58, 36, 2, 59, 37], + [4, 46, 20, 6, 47, 21], + [7, 42, 14, 4, 43, 15], + + // 13 + [4, 133, 107], + [8, 59, 37, 1, 60, 38], + [8, 44, 20, 4, 45, 21], + [12, 33, 11, 4, 34, 12], + + // 14 + [3, 145, 115, 1, 146, 116], + [4, 64, 40, 5, 65, 41], + [11, 36, 16, 5, 37, 17], + [11, 36, 12, 5, 37, 13], + + // 15 + [5, 109, 87, 1, 110, 88], + [5, 65, 41, 5, 66, 42], + [5, 54, 24, 7, 55, 25], + [11, 36, 12, 7, 37, 13], + + // 16 + [5, 122, 98, 1, 123, 99], + [7, 73, 45, 3, 74, 46], + [15, 43, 19, 2, 44, 20], + [3, 45, 15, 13, 46, 16], + + // 17 + [1, 135, 107, 5, 136, 108], + [10, 74, 46, 1, 75, 47], + [1, 50, 22, 15, 51, 23], + [2, 42, 14, 17, 43, 15], + + // 18 + [5, 150, 120, 1, 151, 121], + [9, 69, 43, 4, 70, 44], + [17, 50, 22, 1, 51, 23], + [2, 42, 14, 19, 43, 15], + + // 19 + [3, 141, 113, 4, 142, 114], + [3, 70, 44, 11, 71, 45], + [17, 47, 21, 4, 48, 22], + [9, 39, 13, 16, 40, 14], + + // 20 + [3, 135, 107, 5, 136, 108], + [3, 67, 41, 13, 68, 42], + [15, 54, 24, 5, 55, 25], + [15, 43, 15, 10, 44, 16], + + // 21 + [4, 144, 116, 4, 145, 117], + [17, 68, 42], + [17, 50, 22, 6, 51, 23], + [19, 46, 16, 6, 47, 17], + + // 22 + [2, 139, 111, 7, 140, 112], + [17, 74, 46], + [7, 54, 24, 16, 55, 25], + [34, 37, 13], + + // 23 + [4, 151, 121, 5, 152, 122], + [4, 75, 47, 14, 76, 48], + [11, 54, 24, 14, 55, 25], + [16, 45, 15, 14, 46, 16], + + // 24 + [6, 147, 117, 4, 148, 118], + [6, 73, 45, 14, 74, 46], + [11, 54, 24, 16, 55, 25], + [30, 46, 16, 2, 47, 17], + + // 25 + [8, 132, 106, 4, 133, 107], + [8, 75, 47, 13, 76, 48], + [7, 54, 24, 22, 55, 25], + [22, 45, 15, 13, 46, 16], + + // 26 + [10, 142, 114, 2, 143, 115], + [19, 74, 46, 4, 75, 47], + [28, 50, 22, 6, 51, 23], + [33, 46, 16, 4, 47, 17], + + // 27 + [8, 152, 122, 4, 153, 123], + [22, 73, 45, 3, 74, 46], + [8, 53, 23, 26, 54, 24], + [12, 45, 15, 28, 46, 16], + + // 28 + [3, 147, 117, 10, 148, 118], + [3, 73, 45, 23, 74, 46], + [4, 54, 24, 31, 55, 25], + [11, 45, 15, 31, 46, 16], + + // 29 + [7, 146, 116, 7, 147, 117], + [21, 73, 45, 7, 74, 46], + [1, 53, 23, 37, 54, 24], + [19, 45, 15, 26, 46, 16], + + // 30 + [5, 145, 115, 10, 146, 116], + [19, 75, 47, 10, 76, 48], + [15, 54, 24, 25, 55, 25], + [23, 45, 15, 25, 46, 16], + + // 31 + [13, 145, 115, 3, 146, 116], + [2, 74, 46, 29, 75, 47], + [42, 54, 24, 1, 55, 25], + [23, 45, 15, 28, 46, 16], + + // 32 + [17, 145, 115], + [10, 74, 46, 23, 75, 47], + [10, 54, 24, 35, 55, 25], + [19, 45, 15, 35, 46, 16], + + // 33 + [17, 145, 115, 1, 146, 116], + [14, 74, 46, 21, 75, 47], + [29, 54, 24, 19, 55, 25], + [11, 45, 15, 46, 46, 16], + + // 34 + [13, 145, 115, 6, 146, 116], + [14, 74, 46, 23, 75, 47], + [44, 54, 24, 7, 55, 25], + [59, 46, 16, 1, 47, 17], + + // 35 + [12, 151, 121, 7, 152, 122], + [12, 75, 47, 26, 76, 48], + [39, 54, 24, 14, 55, 25], + [22, 45, 15, 41, 46, 16], + + // 36 + [6, 151, 121, 14, 152, 122], + [6, 75, 47, 34, 76, 48], + [46, 54, 24, 10, 55, 25], + [2, 45, 15, 64, 46, 16], + + // 37 + [17, 152, 122, 4, 153, 123], + [29, 74, 46, 14, 75, 47], + [49, 54, 24, 10, 55, 25], + [24, 45, 15, 46, 46, 16], + + // 38 + [4, 152, 122, 18, 153, 123], + [13, 74, 46, 32, 75, 47], + [48, 54, 24, 14, 55, 25], + [42, 45, 15, 32, 46, 16], + + // 39 + [20, 147, 117, 4, 148, 118], + [40, 75, 47, 7, 76, 48], + [43, 54, 24, 22, 55, 25], + [10, 45, 15, 67, 46, 16], + + // 40 + [19, 148, 118, 6, 149, 119], + [18, 75, 47, 31, 76, 48], + [34, 54, 24, 34, 55, 25], + [20, 45, 15, 61, 46, 16] + ]; + + var qrRSBlock = function(totalCount, dataCount) { + var _this = {}; + _this.totalCount = totalCount; + _this.dataCount = dataCount; + return _this; + }; + + var _this = {}; + + var getRsBlockTable = function(typeNumber, errorCorrectLevel) { + + switch(errorCorrectLevel) { + case QRErrorCorrectLevel.L : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; + case QRErrorCorrectLevel.M : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; + case QRErrorCorrectLevel.Q : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; + case QRErrorCorrectLevel.H : + return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; + default : + return undefined; + } + }; + + _this.getRSBlocks = function(typeNumber, errorCorrectLevel) { + + var rsBlock = getRsBlockTable(typeNumber, errorCorrectLevel); + + if (typeof rsBlock == 'undefined') { + throw new Error('bad rs block @ typeNumber:' + typeNumber + + '/errorCorrectLevel:' + errorCorrectLevel); + } + + var length = rsBlock.length / 3; + + var list = new Array(); + + for (var i = 0; i < length; i += 1) { + + var count = rsBlock[i * 3 + 0]; + var totalCount = rsBlock[i * 3 + 1]; + var dataCount = rsBlock[i * 3 + 2]; + + for (var j = 0; j < count; j += 1) { + list.push(qrRSBlock(totalCount, dataCount) ); + } + } + + return list; + }; + + return _this; + }(); + + //--------------------------------------------------------------------- + // qrBitBuffer + //--------------------------------------------------------------------- + + var qrBitBuffer = function() { + + var _buffer = new Array(); + var _length = 0; + + var _this = {}; + + _this.getBuffer = function() { + return _buffer; + }; + + _this.getAt = function(index) { + var bufIndex = Math.floor(index / 8); + return ( (_buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1; + }; + + _this.put = function(num, length) { + for (var i = 0; i < length; i += 1) { + _this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1); + } + }; + + _this.getLengthInBits = function() { + return _length; + }; + + _this.putBit = function(bit) { + + var bufIndex = Math.floor(_length / 8); + if (_buffer.length <= bufIndex) { + _buffer.push(0); + } + + if (bit) { + _buffer[bufIndex] |= (0x80 >>> (_length % 8) ); + } + + _length += 1; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // qr8BitByte + //--------------------------------------------------------------------- + + var qr8BitByte = function(data) { + + var _mode = QRMode.MODE_8BIT_BYTE; + var _data = data; + var _bytes = qrcode.stringToBytes(data); + + var _this = {}; + + _this.getMode = function() { + return _mode; + }; + + _this.getLength = function(buffer) { + return _bytes.length; + }; + + _this.write = function(buffer) { + for (var i = 0; i < _bytes.length; i += 1) { + buffer.put(_bytes[i], 8); + } + }; + + return _this; + }; + + //===================================================================== + // GIF Support etc. + // + + //--------------------------------------------------------------------- + // byteArrayOutputStream + //--------------------------------------------------------------------- + + var byteArrayOutputStream = function() { + + var _bytes = new Array(); + + var _this = {}; + + _this.writeByte = function(b) { + _bytes.push(b & 0xff); + }; + + _this.writeShort = function(i) { + _this.writeByte(i); + _this.writeByte(i >>> 8); + }; + + _this.writeBytes = function(b, off, len) { + off = off || 0; + len = len || b.length; + for (var i = 0; i < len; i += 1) { + _this.writeByte(b[i + off]); + } + }; + + _this.writeString = function(s) { + for (var i = 0; i < s.length; i += 1) { + _this.writeByte(s.charCodeAt(i) ); + } + }; + + _this.toByteArray = function() { + return _bytes; + }; + + _this.toString = function() { + var s = ''; + s += '['; + for (var i = 0; i < _bytes.length; i += 1) { + if (i > 0) { + s += ','; + } + s += _bytes[i]; + } + s += ']'; + return s; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // base64EncodeOutputStream + //--------------------------------------------------------------------- + + var base64EncodeOutputStream = function() { + + var _buffer = 0; + var _buflen = 0; + var _length = 0; + var _base64 = ''; + + var _this = {}; + + var writeEncoded = function(b) { + _base64 += String.fromCharCode(encode(b & 0x3f) ); + }; + + var encode = function(n) { + if (n < 0) { + // error. + } else if (n < 26) { + return 0x41 + n; + } else if (n < 52) { + return 0x61 + (n - 26); + } else if (n < 62) { + return 0x30 + (n - 52); + } else if (n == 62) { + return 0x2b; + } else if (n == 63) { + return 0x2f; + } + throw new Error('n:' + n); + }; + + _this.writeByte = function(n) { + + _buffer = (_buffer << 8) | (n & 0xff); + _buflen += 8; + _length += 1; + + while (_buflen >= 6) { + writeEncoded(_buffer >>> (_buflen - 6) ); + _buflen -= 6; + } + }; + + _this.flush = function() { + + if (_buflen > 0) { + writeEncoded(_buffer << (6 - _buflen) ); + _buffer = 0; + _buflen = 0; + } + + if (_length % 3 != 0) { + // padding + var padlen = 3 - _length % 3; + for (var i = 0; i < padlen; i += 1) { + _base64 += '='; + } + } + }; + + _this.toString = function() { + return _base64; + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // base64DecodeInputStream + //--------------------------------------------------------------------- + + var base64DecodeInputStream = function(str) { + + var _str = str; + var _pos = 0; + var _buffer = 0; + var _buflen = 0; + + var _this = {}; + + _this.read = function() { + + while (_buflen < 8) { + + if (_pos >= _str.length) { + if (_buflen == 0) { + return -1; + } + throw new Error('unexpected end of file./' + _buflen); + } + + var c = _str.charAt(_pos); + _pos += 1; + + if (c == '=') { + _buflen = 0; + return -1; + } else if (c.match(/^\s$/) ) { + // ignore if whitespace. + continue; + } + + _buffer = (_buffer << 6) | decode(c.charCodeAt(0) ); + _buflen += 6; + } + + var n = (_buffer >>> (_buflen - 8) ) & 0xff; + _buflen -= 8; + return n; + }; + + var decode = function(c) { + if (0x41 <= c && c <= 0x5a) { + return c - 0x41; + } else if (0x61 <= c && c <= 0x7a) { + return c - 0x61 + 26; + } else if (0x30 <= c && c <= 0x39) { + return c - 0x30 + 52; + } else if (c == 0x2b) { + return 62; + } else if (c == 0x2f) { + return 63; + } else { + throw new Error('c:' + c); + } + }; + + return _this; + }; + + //--------------------------------------------------------------------- + // gifImage (B/W) + //--------------------------------------------------------------------- + + var gifImage = function(width, height) { + + var _width = width; + var _height = height; + var _data = new Array(width * height); + + var _this = {}; + + _this.setPixel = function(x, y, pixel) { + _data[y * _width + x] = pixel; + }; + + _this.write = function(out) { + + //--------------------------------- + // GIF Signature + + out.writeString('GIF87a'); + + //--------------------------------- + // Screen Descriptor + + out.writeShort(_width); + out.writeShort(_height); + + out.writeByte(0x80); // 2bit + out.writeByte(0); + out.writeByte(0); + + //--------------------------------- + // Global Color Map + + // black + out.writeByte(0x00); + out.writeByte(0x00); + out.writeByte(0x00); + + // white + out.writeByte(0xff); + out.writeByte(0xff); + out.writeByte(0xff); + + //--------------------------------- + // Image Descriptor + + out.writeString(','); + out.writeShort(0); + out.writeShort(0); + out.writeShort(_width); + out.writeShort(_height); + out.writeByte(0); + + //--------------------------------- + // Local Color Map + + //--------------------------------- + // Raster Data + + var lzwMinCodeSize = 2; + var raster = getLZWRaster(lzwMinCodeSize); + + out.writeByte(lzwMinCodeSize); + + var offset = 0; + + while (raster.length - offset > 255) { + out.writeByte(255); + out.writeBytes(raster, offset, 255); + offset += 255; + } + + out.writeByte(raster.length - offset); + out.writeBytes(raster, offset, raster.length - offset); + out.writeByte(0x00); + + //--------------------------------- + // GIF Terminator + out.writeString(';'); + }; + + var bitOutputStream = function(out) { + + var _out = out; + var _bitLength = 0; + var _bitBuffer = 0; + + var _this = {}; + + _this.write = function(data, length) { + + if ( (data >>> length) != 0) { + throw new Error('length over'); + } + + while (_bitLength + length >= 8) { + _out.writeByte(0xff & ( (data << _bitLength) | _bitBuffer) ); + length -= (8 - _bitLength); + data >>>= (8 - _bitLength); + _bitBuffer = 0; + _bitLength = 0; + } + + _bitBuffer = (data << _bitLength) | _bitBuffer; + _bitLength = _bitLength + length; + }; + + _this.flush = function() { + if (_bitLength > 0) { + _out.writeByte(_bitBuffer); + } + }; + + return _this; + }; + + var getLZWRaster = function(lzwMinCodeSize) { + + var clearCode = 1 << lzwMinCodeSize; + var endCode = (1 << lzwMinCodeSize) + 1; + var bitLength = lzwMinCodeSize + 1; + + // Setup LZWTable + var table = lzwTable(); + + for (var i = 0; i < clearCode; i += 1) { + table.add(String.fromCharCode(i) ); + } + table.add(String.fromCharCode(clearCode) ); + table.add(String.fromCharCode(endCode) ); + + var byteOut = byteArrayOutputStream(); + var bitOut = bitOutputStream(byteOut); + + // clear code + bitOut.write(clearCode, bitLength); + + var dataIndex = 0; + + var s = String.fromCharCode(_data[dataIndex]); + dataIndex += 1; + + while (dataIndex < _data.length) { + + var c = String.fromCharCode(_data[dataIndex]); + dataIndex += 1; + + if (table.contains(s + c) ) { + + s = s + c; + + } else { + + bitOut.write(table.indexOf(s), bitLength); + + if (table.size() < 0xfff) { + + if (table.size() == (1 << bitLength) ) { + bitLength += 1; + } + + table.add(s + c); + } + + s = c; + } + } + + bitOut.write(table.indexOf(s), bitLength); + + // end code + bitOut.write(endCode, bitLength); + + bitOut.flush(); + + return byteOut.toByteArray(); + }; + + var lzwTable = function() { + + var _map = {}; + var _size = 0; + + var _this = {}; + + _this.add = function(key) { + if (_this.contains(key) ) { + throw new Error('dup key:' + key); + } + _map[key] = _size; + _size += 1; + }; + + _this.size = function() { + return _size; + }; + + _this.indexOf = function(key) { + return _map[key]; + }; + + _this.contains = function(key) { + return typeof _map[key] != 'undefined'; + }; + + return _this; + }; + + return _this; + }; + + var createImgTag = function(width, height, getPixel, alt) { + + var gif = gifImage(width, height); + for (var y = 0; y < height; y += 1) { + for (var x = 0; x < width; x += 1) { + gif.setPixel(x, y, getPixel(x, y) ); + } + } + + var b = byteArrayOutputStream(); + gif.write(b); + + var base64 = base64EncodeOutputStream(); + var bytes = b.toByteArray(); + for (var i = 0; i < bytes.length; i += 1) { + base64.writeByte(bytes[i]); + } + base64.flush(); + + var img = ''; + img += '', + link: function(scope, element, attrs) { + var domElement = element[0], + $canvas = element.find('canvas'), + canvas = $canvas[0], + context = canvas2D ? canvas.getContext('2d') : null, + download = 'download' in attrs, + href = attrs.href, + link = download || href ? document.createElement('a') : '', + trim = /^\s+|\s+$/g, + error, + version, + errorCorrectionLevel, + data, + size, + modules, + tile, + qr, + $img, + setVersion = function(value) { + version = Math.max(1, Math.min(parseInt(value, 10), 10)) || 4; + }, + setErrorCorrectionLevel = function(value) { + errorCorrectionLevel = value in levels ? value : 'M'; + }, + setData = function(value) { + if (!value) { + return; + } + + data = value.replace(trim, ''); + qr = qrcode(version, errorCorrectionLevel); + qr.addData(data); + + try { + qr.make(); + } catch(e) { + error = e.message; + return; + } + + error = false; + modules = qr.getModuleCount(); + }, + setSize = function(value) { + size = parseInt(value, 10) || modules * 2; + tile = size / modules; + canvas.width = canvas.height = size; + }, + render = function() { + if (!qr) { + return; + } + + if (error) { + if (link) { + link.removeAttribute('download'); + link.title = ''; + link.href = '#_'; + } + if (!canvas2D) { + domElement.innerHTML = ''; + } + scope.$emit('qrcode:error', error); + return; + } + + if (download) { + domElement.download = 'qrcode.png'; + domElement.title = 'Download QR code'; + } + + if (canvas2D) { + draw(context, qr, modules, tile); + + if (download) { + domElement.href = canvas.toDataURL('image/png'); + return; + } + } else { + domElement.innerHTML = qr.createImgTag(tile, 0); + $img = element.find('img'); + $img.addClass('qrcode'); + + if (download) { + domElement.href = $img[0].src; + return; + } + } + + if (href) { + domElement.href = href; + } + }; + + if (link) { + link.className = 'qrcode-link'; + $canvas.wrap(link); + domElement = link; + } + + setVersion(attrs.version); + setErrorCorrectionLevel(attrs.errorCorrectionLevel); + setSize(attrs.size); + + attrs.$observe('version', function(value) { + if (!value) { + return; + } + + setVersion(value); + setData(data); + setSize(size); + render(); + }); + + attrs.$observe('errorCorrectionLevel', function(value) { + if (!value) { + return; + } + + setErrorCorrectionLevel(value); + setData(data); + setSize(size); + render(); + }); + + attrs.$observe('data', function(value) { + if (!value) { + return; + } + + setData(value); + setSize(size); + render(); + }); + + attrs.$observe('size', function(value) { + if (!value) { + return; + } + + setSize(value); + render(); + }); + + attrs.$observe('href', function(value) { + if (!value) { + return; + } + + href = value; + render(); + }); + } + }; + }]); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/login.html b/docs-web/src/main/webapp/src/partial/docs/login.html index 4aabda53..d03c33ed 100644 --- a/docs-web/src/main/webapp/src/partial/docs/login.html +++ b/docs-web/src/main/webapp/src/partial/docs/login.html @@ -17,6 +17,15 @@ + + A validation code is required + + +
+ + +
+
- -
+
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.security.disabletotp.html b/docs-web/src/main/webapp/src/partial/docs/settings.security.disabletotp.html new file mode 100644 index 00000000..9d13d383 --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/settings.security.disabletotp.html @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.security.html b/docs-web/src/main/webapp/src/partial/docs/settings.security.html new file mode 100644 index 00000000..410ebf10 --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/settings.security.html @@ -0,0 +1,43 @@ +

+ Two-factor authentication + + {{ user.totp_enabled ? 'Enabled' : 'Disabled' }} + +

+ +
+

+ Two-factor authentication allows you to add a layer of security on your Sismics Docs account.
+ Before activating this feature, make sure you have a TOTP-compatible app on your phone: +

+ +

+ Those applications automatically generate a validation code that changes after a certain period of time.
+ You will be required to enter this validation code each time you login on Sismics Docs. +

+

+ +

+
+ +
+
+

Your secret key is: {{ secret }}

+ +

+ Configure your TOTP app on your phone with this secret key now, you will not be able to access it later. +

+
+

+ Two-factor authentication is enabled on your account.
+ Each time you login on Sismics Docs, you will be asked a validation code from your configured phone app.
+ If you loose your phone, you will not be able to login into your account but active sessions will allow you to regenerate a secrey key. +

+

+ +

+
\ No newline at end of file diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index 588f939b..c9428c93 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -240,5 +240,38 @@ input[readonly].share-link { .login-box { background: rgba(255, 255, 255, 0.5); padding: 20px; - border-radius: 4px + border-radius: 4px; + + .help-block, .checkbox { + color: white; + } +} + +/* Styling for the ngProgress itself */ +#ngProgress { + margin: 0; + padding: 0; + z-index: 99998; + background-color: green; + color: green; + box-shadow: 0 0 10px 0; /* Inherits the font color */ + height: 2px; + opacity: 0; + + /* Add CSS3 styles for transition smoothing */ + -webkit-transition: all 0.5s ease-in-out; + -moz-transition: all 0.5s ease-in-out; + -o-transition: all 0.5s ease-in-out; + transition: all 0.5s ease-in-out; +} + +/* Styling for the ngProgress-container */ +#ngProgress-container { + position: fixed; + margin: 0; + padding: 0; + top: 0; + left: 0; + right: 0; + z-index: 99999; } \ No newline at end of file diff --git a/docs-web/src/main/webapp/style/theme/default.less b/docs-web/src/main/webapp/style/theme/default.less deleted file mode 100644 index e69de29b..00000000 diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 44ddb414..04b5153a 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/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/stress/resources/config.properties b/docs-web/src/stress/resources/config.properties index 44ddb414..04b5153a 100644 --- a/docs-web/src/stress/resources/config.properties +++ b/docs-web/src/stress/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/test/java/com/sismics/docs/rest/TestAclResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java index 1da249fe..bace64ef 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java @@ -23,8 +23,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestAclResource extends BaseJerseyTest { /** * Test the ACL resource. - * - * @throws JSONException */ @Test public void testAclResource() { diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java index e6cf2e40..4a8df5b0 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java @@ -21,8 +21,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestAppResource extends BaseJerseyTest { /** * Test the API resource. - * - * @throws JSONException */ @Test public void testAppResource() { @@ -63,8 +61,6 @@ public class TestAppResource extends BaseJerseyTest { /** * Test the log resource. - * - * @throws JSONException */ @Test public void testLogResource() { diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java index 3ec5f011..58ad2e75 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java @@ -20,8 +20,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestAuditLogResource extends BaseJerseyTest { /** * Test the audit log resource. - * - * @throws JSONException */ @Test public void testAuditLogResource() { diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java index f4677dd9..d3a7c1f4 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java @@ -21,11 +21,9 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestCommentResource extends BaseJerseyTest { /** * Test the comment resource. - * - * @throws Exception */ @Test - public void testCommentResource() throws Exception { + public void testCommentResource() { // Login comment1 clientUtil.createUser("comment1"); String comment1Token = clientUtil.login("comment1"); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index b48dd754..4f7b4ee8 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -359,9 +359,8 @@ public class TestDocumentResource extends BaseJerseyTest { * @param query Search query * @param token Authentication token * @return Number of documents found - * @throws Exception */ - private int searchDocuments(String query, String token) throws Exception { + private int searchDocuments(String query, String token) { JsonObject json = target().path("/document/list") .queryParam("search", query) .request() diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index 93cd5a5c..a92f111b 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -198,6 +198,11 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertEquals(1, files.size()); } + /** + * Test orphan files (without linked document). + * + * @throws Exception + */ @Test public void testOrphanFile() throws Exception { // Login file2 @@ -283,6 +288,11 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertEquals("ok", json.getString("status")); } + /** + * Test user quota. + * + * @throws Exception + */ @Test public void testQuota() throws Exception { // Login file_quota diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java index 834c1261..c37ad04c 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java @@ -22,8 +22,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestGroupResource extends BaseJerseyTest { /** * Test the group resource. - * - * @throws JSONException */ @Test public void testGroupResource() { diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java b/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java index 0b938a64..ccf1f6c9 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java @@ -21,8 +21,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestSecurity extends BaseJerseyTest { /** * Test of the security layer. - * - * @throws JSONException */ @Test public void testSecurity() { diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java index 4bccaf43..959e9dd3 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java @@ -28,7 +28,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestShareResource extends BaseJerseyTest { /** * Test the share resource. - * @throws Exception * * @throws Exception */ diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java index 49f68a9d..597074da 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java @@ -21,8 +21,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestTagResource extends BaseJerseyTest { /** * Test the tag resource. - * - * @throws JSONException */ @Test public void testTagResource() { diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index aca82154..3ed9bcb5 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -1,5 +1,6 @@ package com.sismics.docs.rest; +import java.util.Date; import java.util.Locale; import javax.json.JsonArray; @@ -13,6 +14,7 @@ import org.junit.Assert; import org.junit.Test; import com.sismics.util.filter.TokenBasedSecurityFilter; +import com.sismics.util.totp.GoogleAuthenticator; /** * Exhaustive test of the user resource. @@ -22,8 +24,6 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestUserResource extends BaseJerseyTest { /** * Test the user resource. - * - * @throws JSONException */ @Test public void testUserResource() { @@ -229,8 +229,6 @@ public class TestUserResource extends BaseJerseyTest { /** * Test the user resource admin functions. - * - * @throws JSONException */ @Test public void testUserResourceAdmin() { @@ -290,4 +288,71 @@ public class TestUserResource extends BaseJerseyTest { json = response.readEntity(JsonObject.class); Assert.assertEquals("UserNotFound", json.getString("type")); } + + @Test + public void testTotp() { + // Create totp1 user + clientUtil.createUser("totp1"); + String totp1Token = clientUtil.login("totp1"); + + // Check TOTP enablement + JsonObject json = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) + .get(JsonObject.class); + Assert.assertFalse(json.getBoolean("totp_enabled")); + + // Enable TOTP for totp1 + json = target().path("/user/enable_totp").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) + .post(Entity.form(new Form()), JsonObject.class); + String secret = json.getString("secret"); + Assert.assertNotNull(secret); + + // Try to login with totp1 without a validation code + Response response = target().path("/user/login").request() + .post(Entity.form(new Form() + .param("username", "totp1") + .param("password", "12345678") + .param("remember", "false"))); + Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + json = response.readEntity(JsonObject.class); + Assert.assertEquals("ValidationCodeRequired", json.getString("type")); + + // Generate a OTP + GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator(); + int validationCode = googleAuthenticator.calculateCode(secret, new Date().getTime() / 30000); + + // Login with totp1 with a validation code + json = target().path("/user/login").request() + .post(Entity.form(new Form() + .param("username", "totp1") + .param("password", "12345678") + .param("code", Integer.toString(validationCode)) + .param("remember", "false")), JsonObject.class); + + // Check TOTP enablement + json = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) + .get(JsonObject.class); + Assert.assertTrue(json.getBoolean("totp_enabled")); + + // Disable TOTP for totp1 + json = target().path("/user/disable_totp").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) + .post(Entity.form(new Form() + .param("password", "12345678")), JsonObject.class); + + // Login with totp1 without a validation code + json = target().path("/user/login").request() + .post(Entity.form(new Form() + .param("username", "totp1") + .param("password", "12345678") + .param("remember", "false")), JsonObject.class); + + // Check TOTP enablement + json = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, totp1Token) + .get(JsonObject.class); + Assert.assertFalse(json.getBoolean("totp_enabled")); + } } \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java index 9d2005e3..9fd83517 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java @@ -19,11 +19,9 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; public class TestVocabularyResource extends BaseJerseyTest { /** * Test the vocabulary resource. - * - * @throws Exception */ @Test - public void testVocabularyResource() throws Exception { + public void testVocabularyResource() { // Login vocabulary1 clientUtil.createUser("vocabulary1"); String vocabulary1Token = clientUtil.login("vocabulary1");