diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 3e8b6e34..00000000 --- a/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Dependencies -/node_modules - -# Production -/build - -#PDF Assets -/static/assets/pdfs - -# Generated files -.docusaurus -.cache-loader - -# Misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -.obsidian \ No newline at end of file diff --git a/docs/blackboard/sis/sis-password-hashes.md b/docs/blackboard/sis/sis-password-hashes.md new file mode 100644 index 00000000..dd80eb68 --- /dev/null +++ b/docs/blackboard/sis/sis-password-hashes.md @@ -0,0 +1,256 @@ +--- +title: "SIS Password Hash Security" +sidebar_position: 2 +id: sis-password-hashes +author: John Z. Walthall +published: "" +edited: "" +--- + +## Introduction + +You can create or set user passwords by SIS using the 'Person' data type. There are three ways to do this: + +1. Plaintext (not recommended) +2. MD5 (not recommended) +3. Salted SHA-1 ("SSHA", recommended) + +Here is an example of all three formats. (In each case: the password is 'cyan') + +``` +user_id|external_person_key|lastname|firstname|passwd|pwencryptiontype|data_source_key +jshaw|jshaw|Shaw|James|{SSHA}foV2dGZ/2FLNdmJUNEpXZ8ijfiGAriwuB9AYrQ==|SSHA|exterminate +jplain|jplain|Plain|Jane|cyan||exterminate +md5|md5|Five|Maddy|6411532ba4971f378391776a9db629d3|MD5|exterminate +``` + +## Supplying a Password Hash + +These hash formats are not the one used by the LMS. When a password is set in the GUI it is hashed using a +[512-bit SHA-2-based](https://help.blackboard.com/Learn/Administrator/SaaS/Security/Identification_Authentication) hash. + +If you supply the field `passwd`, but not the field `pwencryptiontype` (or: set this field to blank) the input will be +hashed by the LMS using the aforementioned hash. + +Otherwise, the input will be copied verbatim into the database. When the user logs in: which is the only time the clear- +text of the password is accessible to Blackboard, the cleartext will be re-hashed using a strong hash and the existing +one overwritten. + +If you set 'change on update' for the password field: the stronger hash will be overwritten. + +### Using MD5 + +:::danger +It's not recommended to use MD5. MD5 is obsolete and no longer secure. It may be subject to removal in a future version +of Blackboard Learn. +::: + +The MD5 hash is trivial: it's simply the standard hex representation of the MD5sum of the string. So if the password is +'cyan' then + +```shell +$ echo -n "cyan" | md5 +6411532ba4971f378391776a9db629d3 +``` + +### Using SSHA + +This format is an informal standard and our implementation is _similar to_ but _not the same as_ [the format used by +`slappasswd(8)`](https://github.com/openldap/openldap/blob/34813d9cba02a74216a784636a8d5f0f986d73c7/libraries/liblutil/passwd.c#L749-L779) +from the OpenLDAP project. The key difference is the salt-size. Blackboard Learn uses an **8-byte** salt. OpenLDAP uses +a **4-byte** salt. Since the salt-size is not stored in the format, hashes with different salt-lengths are not +compatible. +Therefore, the hashes generated by `slappasswd(8)` cannot be used. + +### Algorithm Description + +1. For each password: create 8 random bytes of 'salt'. Never re-use salts. Use a Cryptographically Secure Psuedo-Random + Number Generator, or a true environmentally sourced random-number generator. +2. Convert the password string to a byte array using UTF-8. +3. Digest the concatenation of the password bytes + the salt bytes as SHA-1. The SHA-1 digest must be a byte array + itself, if the implementation produces a hex string, it must be converted to a byte array by parsing each pair of + characters as an unsigned hexadecimal byte. +4. Append another copy of the same salt bytes. +5. Encode this as Base64 in ASCII. Use the default dictionary: not the 'url-safe' one. +7. Prefix with `{SSHA}`. + +### Step by Step + +1. Let the password be `nucleus` +2. Let the salt be `[21, F0, 25, 09, 15, D2, 68, 1F]` (interpreted as an array of unsigned bytes.) Remember: it must + always be eight new random bytes. +3. The UTF-8 bytes of 'nucleus' are `[6E, 75, 63, 6C, 65, 75, 73]` +4. Thus the value to be digested is `[6E, 75, 63, 6C, 65, 75, 73, 21, F0, 25, 09, 15, D2, 68, 1F]` +5. The SHA-1 digest of this (interpreted as an array of unsigned bytes) is + `[90, FC, 6D, A2, C9, EA, 04, 10, 83, 20, C4, AC, 15, 73, A7, 49, BD, 88, 7A, 63]` +6. Appending the salt to that is + `[90, FC, 6D, A2, C9, EA, 04, 10, 83, 20, C4, AC, 15, 73, A7, 49, BD, 88, 7A, 63, 21, F0, 25, 09, 15, D2, 68, 1F]` +7. The Base64 encoding of this, with prefix is `{SSHA}kPxtosnqBBCDIMSsFXOnSb2IemMh8CUJFdJoHw==` + +### Code Examples + +Here are some code examples. + +:::caution +A particular source of confusion, and many examples on the Internet stumble on this confusion, is that the SHA-1 output +must be bytes, not a hex string. If the programming language or library you choose returns a hex string for SHA-1, then +it must be recast as a byte array. That is to say: `EF` must be a single byte: `239` (unsigned), not a pair of bytes +equal to unsigned `69` (the '`E`') and `70` (the '`F`'.) +::: + +:::info +According to the SHA-1 specification: an empty string can be hashed. You must not do this. In the SIS API, an empty +password indicates the LMS should generate a random password. Setting the password to the SSHA hash of empty string +results in a blank password being set. +::: + +#### Java + +```java +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; + +/** + * Encoder for our nonstandard SSHA variant using 8-byte salt. + */ +public final class VariantSSHA { + + /** + * Encode string to the hash + * @param password The password to encode + * @return the variant-SSHA hash. + */ + public static String variantSSHAEncode(String password) { + if (password.isBlank()) { + // empty-string password has a special meaning in Learn. But a hash of empty-string is a valid hash + // bail out to preserve semantics + throw new IllegalArgumentException("Password cannot be blank"); + } + // The salt is always 8 bytes. This is incompatible with slappasswd(8) + byte[] salt = new byte[8]; + SecureRandom rand = null; + try { + rand = SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("FATAL: can't load secure randomness", e); + } + rand.nextBytes(salt); + + byte[] bytesUTF8 = password.getBytes(StandardCharsets.UTF_8); + + try { + // hash the combination of the password + the salt. + MessageDigest sha1Digest = MessageDigest.getInstance("SHA1"); + sha1Digest.update(bytesUTF8); + sha1Digest.update(salt); + byte[] binaryHash = sha1Digest.digest(); + + // append the salt to the hash + byte[] hashPlusSalt = new byte[binaryHash.length + salt.length]; + System.arraycopy(binaryHash, 0, hashPlusSalt, 0, binaryHash.length); + System.arraycopy(salt, 0, hashPlusSalt, binaryHash.length, salt.length); + + // This is mostly security theater; but customary + Arrays.fill(salt, (byte) 0); + Arrays.fill(bytesUTF8, (byte) 0); + + String stringHash = "{SSHA}" + Base64.getEncoder().encodeToString(hashPlusSalt); + Arrays.fill(hashPlusSalt, (byte) 0); + + return stringHash; + } catch (NoSuchAlgorithmException e) { + // can't happen. JSSE requires all implementing runtimes to support SHA-1 + throw new RuntimeException(e); + } + } + + /** + * Convenience method to use as a simple utility. Output format is {@code original_password + tab + SSHA} + * + *
+     * $ java VariantSSHA.java the quick brown fox" "jumps over the lazy dog" "when zombies arrive" "quickly fax judge patty"
+     *     the quick brown fox	{SSHA}r+QLZ86dFWWp0oXhGC3nW5U/p08DvFVyKH1M/w==
+     * jumps over the lazy dog	{SSHA}yxUScjSM42EBpL2qB7I2wLf/CLHBQX0No18z/w==
+     *     when zombies arrive	{SSHA}Yvot6sr1F7XNahlwY0KeXmmukpw19oYSJnZhRQ==
+     * quickly fax judge patty	{SSHA}f01o7IJGet6TzvizERwuVzPX7Ud09Pu3HGJeZg==
+     * 
+ * @param args strings to encode. + */ + static void main(String... args) { + if (args.length == 0) { + System.err.println("Try again"); + System.err.println("Usage: java VariantSSHA password1 password2 password3..."); + } else { + int pad = Arrays.stream(args).mapToInt(String::length).max().orElse(10); + for (String password : args) { + System.out.printf("%" + pad + "s\t%s%n", password, variantSSHAEncode(password)); + } + } + } +} +``` + +#### Python + +```python +#!/usr/bin/env python3 +""" +Generate Blackboard's nonstandard 8-byte-salted SSHA variant. +""" +import base64 +import hashlib +import os +import sys + + +def variant_ssha_encode(password: str): + if password == "": + raise RuntimeError("Password can not be empty") + + # Generate 8 random bytes of salt. os.urandom is cryptographically secure + salt = os.urandom(8) + + # convert the password to bytes + bytes_utf8 = password.encode("utf-8") + + sha1digest = hashlib.sha1() + + # equivalent to byte_utf8 + salt + sha1digest.update(bytes_utf8) + sha1digest.update(salt) + + # append the salt to the end of the SHA-1 digest (note the digest must be bytes, not hex string) + hash_with_salt = sha1digest.digest() + salt + + # encode to base64, pre-fix the identifier and return. + return "{SSHA}" + base64.b64encode(hash_with_salt).decode("ascii") + + +def main(): + """ + Convenience method to use as a simple utility + invocation: + $ python3 generate_ssha_hash.py "the quick brown fox" "jumps over the lazy dog" "when zombies arrive" "quickly fax judge patty" + the quick brown fox {SSHA}Ffy5dpkMeMIiebd+Sqtu0FJOV6xdAh4Wp9aeSA== + jumps over the lazy dog {SSHA}SmYwGocJidrBS9AfBid9P/JUUOxhTZLylWcKQw== + when zombies arrive {SSHA}layQWCu+uVrFmXeKE4ZeqPGzCJ87OVI0zAnjJQ== + quickly fax judge patty {SSHA}IJbtvQYh6TocBq5m4yoU0sVRvUdMrR+hZUHxCQ== + """ + if len(sys.argv) == 1: + print("Try again\nUsage: python3 generate_ssha_hash.py password1 password2 password3...") + sys.exit(1) + passwords = sys.argv[1:] + pad = len(max(passwords, key=len)) + for pw in passwords: + ssha = variant_ssha_encode(pw) + padded = pw.rjust(pad) + print(f"{padded}\t{ssha}") + + +if __name__ == "__main__": + main() +``` \ No newline at end of file diff --git a/docs/blackboard/sis/welcome.md b/docs/blackboard/sis/welcome.md new file mode 100644 index 00000000..5a94f16f --- /dev/null +++ b/docs/blackboard/sis/welcome.md @@ -0,0 +1,36 @@ +--- +title: "Getting started with SIS" +sidebar_position: 1 +id: getting-started +author: John Z. Walthall +published: "" +edited: "" +--- + +The SIS (Student Information Systems) API is a classic API oriented around bulk or event-driven data loading from a SIS +or ERP program like Anthology Student, Banner or Colleague. Most aspects of SIS are [documented on our main help site](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System), +but some developer documentation is provided here. + +## SIS Types + +The "prime" data type is "[Snapshot Flat File](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System/SIS_Integration_Types/Snapshot_Flat_File)", which is a bulk-loading oriented delimited text file. This is the 'native' +format of Blackboard and has the widest support for the LMS's many data types. + +We also support three industry-standard formats: + +* [LIS 2.0 Final and Draft](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System/SIS_Integration_Types/LIS) +* [IMS Enterprise 1.1](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System/SIS_Integration_Types/Enterprise_1.1) +* [IMS Enterprise 1.1 - Vista](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System/SIS_Integration_Types/Enterprise_1.1) + +These are usually used in an event-driven workflow. + +## Security + +SIS activity occurs at a high level of privilege. It can create, remove, or delete anything. A user that gains access to +an SIS username and password could send arbitrary data files that cause arbitrary changes. + +SIS data files also contain potentially sensitive data and access to them should be strictly controlled. (Note: Support +may require the feed file for problem investigation.) + +One particular case is setting passwords by SIS. They can be supplied in cleartext, but this is not recommended and they +should be provided in [hashed form.](sis-password-hashes.md) \ No newline at end of file diff --git a/sidebar.js b/sidebar.js index 61312b86..297cbb42 100644 --- a/sidebar.js +++ b/sidebar.js @@ -209,6 +209,12 @@ const sidebars = { }, ], }, + { + SIS: [ + "blackboard/sis/getting-started", + "blackboard/sis/sis-password-hashes" + ] + }, ], }, // Student