password service implementation 29/72129/1
authorRyan Goulding <ryandgoulding@gmail.com>
Thu, 17 May 2018 20:16:06 +0000 (16:16 -0400)
committerRyan Goulding <ryandgoulding@gmail.com>
Mon, 21 May 2018 18:59:18 +0000 (14:59 -0400)
Implement PasswordHashService with a Default impl.  This impl is capable
of deriving values from aaa-password-service-config.yang.

Change-Id: I55a6bebcc18ab60b229006ec50b9440292ec5ffb
Signed-off-by: Ryan Goulding <ryandgoulding@gmail.com>
aaa-password-service/api/src/main/yang/aaa-password-service-config.yang [new file with mode: 0644]
aaa-password-service/impl/pom.xml [new file with mode: 0644]
aaa-password-service/impl/src/main/java/org/opendaylight/aaa/impl/password/service/DefaultPasswordHashService.java [new file with mode: 0644]
aaa-password-service/impl/src/main/java/org/opendaylight/aaa/impl/password/service/PasswordHashImpl.java [new file with mode: 0644]
aaa-password-service/impl/src/main/resources/initial/aaa-password-service-config.xml [new file with mode: 0644]
aaa-password-service/impl/src/main/resources/org/opendaylight/blueprint/password-service-blueprint.xml [new file with mode: 0644]
aaa-password-service/impl/src/test/java/org/opendaylight/aaa/impl/password/service/DefaultPasswordHashServiceTest.java [new file with mode: 0644]
aaa-password-service/pom.xml
artifacts/pom.xml

diff --git a/aaa-password-service/api/src/main/yang/aaa-password-service-config.yang b/aaa-password-service/api/src/main/yang/aaa-password-service-config.yang
new file mode 100644 (file)
index 0000000..db4b53f
--- /dev/null
@@ -0,0 +1,33 @@
+module aaa-password-service-config {
+    yang-version 1;
+    namespace "urn:opendaylight:aaa:password:service:config";
+    prefix "aaa-password-service-config";
+    organization "OpenDaylight";
+
+    contact "ryandgoulding@gmail.com";
+
+    revision "2017-06-19" {
+        description "Initial password service configuration.";
+    }
+
+    container password-service-config {
+
+        leaf algorithm {
+            type string;
+            default "SHA-512";
+            description "The algorithm utilized for aaa-password-service hashing.";
+        }
+
+        leaf iterations {
+            type int32;
+            default 20000;
+            description "The number of times to hash.";
+        }
+
+        leaf private-salt {
+            type string;
+            description "The private salt for password hashing.";
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/aaa-password-service/impl/pom.xml b/aaa-password-service/impl/pom.xml
new file mode 100644 (file)
index 0000000..0cfdcf9
--- /dev/null
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright © 2017 Brocade Communications Systems and others. All rights reserved.
+
+This program and the accompanying materials are made available under the
+terms of the Eclipse Public License v1.0 which accompanies this distribution,
+and is available at http://www.eclipse.org/legal/epl-v10.html
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.opendaylight.aaa</groupId>
+    <artifactId>aaa-parent</artifactId>
+    <version>0.8.0-SNAPSHOT</version>
+    <relativePath>../../parent</relativePath>
+  </parent>
+
+  <groupId>org.opendaylight.aaa</groupId>
+  <artifactId>aaa-password-service-impl</artifactId>
+  <version>0.8.0-SNAPSHOT</version>
+  <name>ODL :: aaa :: ${project.artifactId}</name>
+  <packaging>bundle</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.opendaylight.aaa</groupId>
+      <artifactId>aaa-password-service-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shiro</groupId>
+      <artifactId>shiro-core</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>attach-artifacts</id>
+            <phase>package</phase>
+            <goals>
+              <goal>attach-artifact</goal>
+            </goals>
+            <configuration>
+              <artifacts>
+                <!-- attach aaa-app-config.xml as an artifact -->
+                <artifact>
+                  <file>${project.build.directory}/classes/initial/aaa-password-service-config.xml</file>
+                  <type>xml</type>
+                  <classifier>aaa-password-service-config</classifier>
+                </artifact>
+              </artifacts>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/aaa-password-service/impl/src/main/java/org/opendaylight/aaa/impl/password/service/DefaultPasswordHashService.java b/aaa-password-service/impl/src/main/java/org/opendaylight/aaa/impl/password/service/DefaultPasswordHashService.java
new file mode 100644 (file)
index 0000000..151e150
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2018 Inocybe Technologies and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.impl.password.service;
+
+import java.util.Optional;
+import org.apache.shiro.codec.Base64;
+import org.apache.shiro.crypto.SecureRandomNumberGenerator;
+import org.apache.shiro.crypto.hash.DefaultHashService;
+import org.apache.shiro.crypto.hash.Hash;
+import org.apache.shiro.crypto.hash.HashRequest;
+import org.apache.shiro.crypto.hash.SimpleHashRequest;
+import org.apache.shiro.util.ByteSource;
+import org.opendaylight.aaa.api.password.service.PasswordHash;
+import org.opendaylight.aaa.api.password.service.PasswordHashService;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.password.service.config.rev170619.PasswordServiceConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultPasswordHashService implements PasswordHashService {
+
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultPasswordHashService.class);
+
+    public static final String DEFAULT_HASH_ALGORITHM = "SHA-512";
+    public static final int DEFAULT_NUM_ITERATIONS = 10000;
+
+    private final DefaultHashService hashService = new DefaultHashService();
+
+    public DefaultPasswordHashService(final PasswordServiceConfig passwordServiceConfig) {
+        final Optional<Integer> numIterationsOptional = Optional.ofNullable(passwordServiceConfig.getIterations());
+        setNumIterations(numIterationsOptional);
+
+        final Optional<String> hashAlgorithmOptional = Optional.ofNullable(passwordServiceConfig.getAlgorithm());
+        setHashAlgorithm(hashAlgorithmOptional);
+
+        final Optional<String> privateSaltOptional = Optional.ofNullable(passwordServiceConfig.getPrivateSalt());
+        setPrivateSalt(privateSaltOptional);
+
+        hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator());
+        hashService.setGeneratePublicSalt(true);
+    }
+
+    private void setNumIterations(final Optional<Integer> numIterationsOptional) {
+        if (numIterationsOptional.isPresent()) {
+            final Integer numIterations = numIterationsOptional.get();
+            hashService.setHashIterations(numIterations);
+            LOG.info("DefaultPasswordHashService will utilize configured iteration count={}", numIterations);
+        } else {
+            hashService.setHashIterations(DEFAULT_NUM_ITERATIONS);
+            LOG.info("DefaultPasswordHashService will utilize default iteration count={}", DEFAULT_NUM_ITERATIONS);
+        }
+    }
+
+    private void setHashAlgorithm(final Optional<String> hashAlgorithmOptional) {
+        if (hashAlgorithmOptional.isPresent()) {
+            final String hashAlgorithm = hashAlgorithmOptional.get();
+            hashService.setHashAlgorithmName(hashAlgorithm);
+            LOG.info("DefaultPasswordHashService will utilize configured algorithm={}", hashAlgorithm);
+        } else {
+            hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM);
+            LOG.info("DefaultPasswordHashService will utilize default algorithm={}", DEFAULT_HASH_ALGORITHM);
+        }
+    }
+
+    private void setPrivateSalt(final Optional<String> privateSaltOptional) {
+        if (privateSaltOptional.isPresent()) {
+            hashService.setPrivateSalt(ByteSource.Util.bytes(privateSaltOptional.get()));
+            LOG.info("DefaultPasswordHashService will utilize a configured private salt");
+        } else {
+            LOG.info("DefaultPasswordHashService will not utilize a private salt, since none was configured");
+        }
+    }
+
+    @Override
+    public PasswordHash getPasswordHash(final String password) {
+        final HashRequest hashRequest = new HashRequest.Builder()
+                .setAlgorithmName(hashService.getHashAlgorithmName())
+                .setIterations(hashService.getHashIterations())
+                .setSource(ByteSource.Util.bytes(password)).build();
+
+        final Hash hash =  hashService.computeHash(hashRequest);
+        return PasswordHashImpl.create(
+                hash.getAlgorithmName(),
+                hash.getSalt().toBase64(),
+                hash.getIterations(),
+                hash.toBase64());
+    }
+
+    @Override
+    public PasswordHash getPasswordHash(final String password, final String salt) {
+        final HashRequest hashRequest = new SimpleHashRequest(
+                hashService.getHashAlgorithmName(),
+                ByteSource.Util.bytes(password),
+                ByteSource.Util.bytes(Base64.decode(salt)),
+                hashService.getHashIterations());
+
+        final Hash hash =  hashService.computeHash(hashRequest);
+        return PasswordHashImpl.create(
+                hash.getAlgorithmName(),
+                hash.getSalt().toBase64(),
+                hash.getIterations(),
+                hash.toBase64());
+    }
+
+    @Override
+    public boolean passwordsMatch(final String plaintext, final String stored, final String salt) {
+        final String hash = getPasswordHash(plaintext, salt).getHashedPassword();
+        return hash.equals(stored);
+    }
+}
diff --git a/aaa-password-service/impl/src/main/java/org/opendaylight/aaa/impl/password/service/PasswordHashImpl.java b/aaa-password-service/impl/src/main/java/org/opendaylight/aaa/impl/password/service/PasswordHashImpl.java
new file mode 100644 (file)
index 0000000..06faad0
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2018 Inocybe Technologies and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.impl.password.service;
+
+import org.opendaylight.aaa.api.password.service.PasswordHash;
+
+public final class PasswordHashImpl implements PasswordHash {
+
+    private final String algorithmName;
+    private final String salt;
+    private final int iterations;
+    private final String hashedPassword;
+
+    private PasswordHashImpl(final String algorithmName, final String salt, final int iterations,
+                             final String hashedPassword) {
+
+        this.algorithmName = algorithmName;
+        this.salt = salt;
+        this.iterations = iterations;
+        this.hashedPassword = hashedPassword;
+    }
+
+    public static PasswordHash create(final String algorithmName, final String salt, final int iterations,
+                                      final String hashedPassword) {
+
+        return new PasswordHashImpl(algorithmName, salt, iterations, hashedPassword);
+    }
+
+    public String getAlgorithmName() {
+        return this.algorithmName;
+    }
+
+    public String getSalt() {
+        return this.salt;
+    }
+
+    public int getIterations() {
+        return this.iterations;
+    }
+
+    public String getHashedPassword() {
+        return this.hashedPassword;
+    }
+}
\ No newline at end of file
diff --git a/aaa-password-service/impl/src/main/resources/initial/aaa-password-service-config.xml b/aaa-password-service/impl/src/main/resources/initial/aaa-password-service-config.xml
new file mode 100644 (file)
index 0000000..dcabdc2
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<password-service-config xmlns="urn:opendaylight:aaa:password:service:config">
+    <algorithm>SHA-512</algorithm>
+    <iterations>20000</iterations>
+</password-service-config>
\ No newline at end of file
diff --git a/aaa-password-service/impl/src/main/resources/org/opendaylight/blueprint/password-service-blueprint.xml b/aaa-password-service/impl/src/main/resources/org/opendaylight/blueprint/password-service-blueprint.xml
new file mode 100644 (file)
index 0000000..752a1b0
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- vi: set et smarttab sw=4 tabstop=4: -->
+<!--
+Copyright © 2018 Inocybe Technologies and others. All rights reserved.
+
+This program and the accompanying materials are made available under the
+terms of the Eclipse Public License v1.0 which accompanies this distribution,
+and is available at http://www.eclipse.org/legal/epl-v10.html
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+  xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
+  odl:use-default-for-reference-types="true">
+
+    <odl:clustered-app-config
+            binding-class="org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.password.service.config.rev170619.PasswordServiceConfig"
+            id="passwordServiceConfig" default-config-file-name="aaa-password-service-config.xml" />
+
+    <bean id="passwordService" class="org.opendaylight.aaa.impl.password.service.DefaultPasswordHashService">
+        <argument ref="passwordServiceConfig" />
+    </bean>
+    <service ref="passwordService" interface="org.opendaylight.aaa.api.password.service.PasswordService"
+        odl:type="default"/>
+
+</blueprint>
diff --git a/aaa-password-service/impl/src/test/java/org/opendaylight/aaa/impl/password/service/DefaultPasswordHashServiceTest.java b/aaa-password-service/impl/src/test/java/org/opendaylight/aaa/impl/password/service/DefaultPasswordHashServiceTest.java
new file mode 100644 (file)
index 0000000..06b80e2
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2018 Inocybe Technologies and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.impl.password.service;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shiro.codec.Base64;
+import org.junit.Test;
+import org.opendaylight.aaa.api.password.service.PasswordHash;
+import org.opendaylight.aaa.api.password.service.PasswordHashService;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.aaa.password.service.config.rev170619.PasswordServiceConfigBuilder;
+
+public class DefaultPasswordHashServiceTest {
+
+    @Test
+    public void testDefault() {
+        PasswordHashService hashService = new DefaultPasswordHashService(new PasswordServiceConfigBuilder().build());
+        PasswordHash hash = hashService.getPasswordHash("password");
+        assertEquals(DefaultPasswordHashService.DEFAULT_HASH_ALGORITHM, hash.getAlgorithmName());
+        assertEquals(DefaultPasswordHashService.DEFAULT_NUM_ITERATIONS, hash.getIterations());
+        assertNotNull(hash.getSalt());
+        assertNotNull(hash.getHashedPassword());
+
+
+        assertEquals(hash.getSalt(),
+                hashService.getPasswordHash("password", hash.getSalt()).getSalt());
+        // make sure that when utilizing the returned salt, the answer is the same
+        assertEquals(hash.getHashedPassword(),
+                hashService.getPasswordHash("password", hash.getSalt()).getHashedPassword());
+    }
+
+    @Test
+    public void testWithSpecialConfiguration() {
+        String privateSalt = Base64.encodeToString("somePrivateSalt".getBytes());
+        String publicSalt = Base64.encodeToString("somePublicSalt".getBytes());
+        DefaultPasswordHashService hashService = new DefaultPasswordHashService(new PasswordServiceConfigBuilder()
+                .setAlgorithm("MD5")
+                .setIterations(24)
+                .setPrivateSalt(privateSalt)
+                .build());
+        PasswordHash hash = hashService.getPasswordHash("password", publicSalt);
+        assertEquals("MD5", hash.getAlgorithmName());
+        assertEquals(24, hash.getIterations());
+        assertEquals(publicSalt, hash.getSalt());
+        assertNotNull(hash.getHashedPassword());
+        assertEquals(hash.getHashedPassword(),
+                hashService.getPasswordHash("password", hash.getSalt()).getHashedPassword());
+
+        hashService = new DefaultPasswordHashService(new PasswordServiceConfigBuilder()
+                .setAlgorithm("MD5")
+                .setIterations(20)
+                .build());
+        hash = hashService.getPasswordHash("password");
+        assertEquals(hash.getSalt(),
+                hashService.getPasswordHash("password", hash.getSalt()).getSalt());
+        // make sure that when utilizing the returned salt, the answer is the same
+        assertEquals(hash.getHashedPassword(),
+                hashService.getPasswordHash("password", hash.getSalt()).getHashedPassword());
+
+        assertEquals(hashService.getPasswordHash("password", privateSalt).getHashedPassword(),
+                hashService.getPasswordHash("password", privateSalt).getHashedPassword());
+    }
+
+    @Test
+    public void testPasswordsMatch() {
+        PasswordHashService hashService = new DefaultPasswordHashService(new PasswordServiceConfigBuilder().build());
+        PasswordHash passwordHash = hashService.getPasswordHash("password");
+        assertTrue(hashService.passwordsMatch("password",
+                passwordHash.getHashedPassword(), passwordHash.getSalt()));
+    }
+}
\ No newline at end of file
index 786654edd81a9a904f90c304d21c82c1ee65c3d0..0ee3dbc77e7638791a8f9fd6adaaac413da228e6 100644 (file)
@@ -7,6 +7,18 @@ and is available at http://www.eclipse.org/legal/epl-v10.html INTERNAL
 -->
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.shiro</groupId>
+      <artifactId>shiro-core</artifactId>
+      <version>1.3.2</version>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.aaa</groupId>
+      <artifactId>aaa-password-service-api</artifactId>
+      <version>0.8.0-SNAPSHOT</version>
+    </dependency>
+  </dependencies>
 
   <parent>
     <groupId>org.opendaylight.odlparent</groupId>
@@ -23,7 +35,7 @@ and is available at http://www.eclipse.org/legal/epl-v10.html INTERNAL
 
   <modules>
     <module>api</module>
-    <!--<module>impl</module>-->
+    <module>impl</module>
   </modules>
 
 </project>
index 2003872df76d439fc585f8407685e976cb0ff792..a0c648019ac84f3ae6c4736cb462398791240c77 100644 (file)
                 <artifactId>aaa-password-service-api</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.opendaylight.aaa</groupId>
+                <artifactId>aaa-password-service-impl</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.opendaylight.aaa</groupId>
+                <artifactId>aaa-password-service-impl</artifactId>
+                <version>${project.version}</version>
+                <classifier>aaa-password-service-config</classifier>
+                <type>xml</type>
+            </dependency>
             <dependency>
                 <groupId>${project.groupId}</groupId>
                 <artifactId>features-aaa</artifactId>