From 962b241a8093e9510b601ef29869e5079017ee23 Mon Sep 17 00:00:00 2001 From: "John Z. Walthall" Date: Mon, 10 Nov 2025 12:05:11 -0500 Subject: [PATCH 1/5] DRAFT: SIS password hashes --- docs/blackboard/sis/sis-password-hashes.md | 256 +++++++++++++++++++++ docs/blackboard/sis/welcome.md | 34 +++ sidebar.js | 6 + 3 files changed, 296 insertions(+) create mode 100644 docs/blackboard/sis/sis-password-hashes.md create mode 100644 docs/blackboard/sis/welcome.md diff --git a/docs/blackboard/sis/sis-password-hashes.md b/docs/blackboard/sis/sis-password-hashes.md new file mode 100644 index 00000000..32f454c7 --- /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 SHA1, 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..6eddaf99 --- /dev/null +++ b/docs/blackboard/sis/welcome.md @@ -0,0 +1,34 @@ +--- +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 is effectively infinitely powerful. It can create, remove, or delete anything. SIS data files also contain highly +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 1f380a95..69344639 100644 --- a/sidebar.js +++ b/sidebar.js @@ -184,6 +184,12 @@ const sidebars = { }, ], }, + { + SIS: [ + "blackboard/sis/getting-started", + "blackboard/sis/sis-password-hashes" + ] + }, "developer-ami", ], }, From e95203642e26c37e23e22f49c0b832973b738ae4 Mon Sep 17 00:00:00 2001 From: "John Z. Walthall" Date: Wed, 12 Nov 2025 10:31:48 -0500 Subject: [PATCH 2/5] Re-word some things and be consistent with "SHA" vs "SHA-1" --- .gitignore | 25 ---------------------- docs/blackboard/sis/sis-password-hashes.md | 2 +- docs/blackboard/sis/welcome.md | 8 ++++--- 3 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 .gitignore 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 index 32f454c7..dd80eb68 100644 --- a/docs/blackboard/sis/sis-password-hashes.md +++ b/docs/blackboard/sis/sis-password-hashes.md @@ -93,7 +93,7 @@ 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 SHA1, then +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`'.) ::: diff --git a/docs/blackboard/sis/welcome.md b/docs/blackboard/sis/welcome.md index 6eddaf99..5a94f16f 100644 --- a/docs/blackboard/sis/welcome.md +++ b/docs/blackboard/sis/welcome.md @@ -26,9 +26,11 @@ These are usually used in an event-driven workflow. ## Security -SIS is effectively infinitely powerful. It can create, remove, or delete anything. SIS data files also contain highly -sensitive data and access to them should be strictly controlled. (Note: Support may require the feed file for problem -investigation.) +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 From 86b82fe755a65d8cf6d46bd26179caf058871e02 Mon Sep 17 00:00:00 2001 From: "John Z. Walthall" Date: Mon, 10 Nov 2025 12:05:11 -0500 Subject: [PATCH 3/5] SIS password hashes --- docs/blackboard/sis/sis-password-hashes.md | 256 +++++++++++++++++++++ docs/blackboard/sis/welcome.md | 34 +++ sidebar.js | 7 + 3 files changed, 297 insertions(+) create mode 100644 docs/blackboard/sis/sis-password-hashes.md create mode 100644 docs/blackboard/sis/welcome.md diff --git a/docs/blackboard/sis/sis-password-hashes.md b/docs/blackboard/sis/sis-password-hashes.md new file mode 100644 index 00000000..32f454c7 --- /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 SHA1, 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..6eddaf99 --- /dev/null +++ b/docs/blackboard/sis/welcome.md @@ -0,0 +1,34 @@ +--- +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 is effectively infinitely powerful. It can create, remove, or delete anything. SIS data files also contain highly +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..12d7dd51 100644 --- a/sidebar.js +++ b/sidebar.js @@ -209,6 +209,13 @@ const sidebars = { }, ], }, + { + SIS: [ + "blackboard/sis/getting-started", + "blackboard/sis/sis-password-hashes" + ] + }, + "developer-ami", ], }, // Student From 9a858cf171b3cc724e8a205b115c485803a410e2 Mon Sep 17 00:00:00 2001 From: "John Z. Walthall" Date: Wed, 12 Nov 2025 10:31:48 -0500 Subject: [PATCH 4/5] Re-word some things and be consistent with "SHA" vs "SHA-1" --- .gitignore | 25 ---------------------- docs/blackboard/sis/sis-password-hashes.md | 2 +- docs/blackboard/sis/welcome.md | 8 ++++--- 3 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 .gitignore 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 index 32f454c7..dd80eb68 100644 --- a/docs/blackboard/sis/sis-password-hashes.md +++ b/docs/blackboard/sis/sis-password-hashes.md @@ -93,7 +93,7 @@ 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 SHA1, then +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`'.) ::: diff --git a/docs/blackboard/sis/welcome.md b/docs/blackboard/sis/welcome.md index 6eddaf99..5a94f16f 100644 --- a/docs/blackboard/sis/welcome.md +++ b/docs/blackboard/sis/welcome.md @@ -26,9 +26,11 @@ These are usually used in an event-driven workflow. ## Security -SIS is effectively infinitely powerful. It can create, remove, or delete anything. SIS data files also contain highly -sensitive data and access to them should be strictly controlled. (Note: Support may require the feed file for problem -investigation.) +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 From 7ca87420680731c93505f07b07d6786cf786bcb0 Mon Sep 17 00:00:00 2001 From: "John Z. Walthall" Date: Fri, 21 Nov 2025 14:57:29 -0500 Subject: [PATCH 5/5] Remove wrong sidebar ID caused by conflict resolution error --- sidebar.js | 1 - 1 file changed, 1 deletion(-) diff --git a/sidebar.js b/sidebar.js index 12d7dd51..297cbb42 100644 --- a/sidebar.js +++ b/sidebar.js @@ -215,7 +215,6 @@ const sidebars = { "blackboard/sis/sis-password-hashes" ] }, - "developer-ami", ], }, // Student