на примере функции шифрования (с минимальными изменениями, как у Вас):
1. Вынести объявление из цикла;
2. печать так же вынести из цикла;
3. результат не "джойним": у нас и так срока в функции шифрования
def ciph():
string = str(input("Введите строку, которую надо зашифровать: "))
key = int(input("Введите ключ шифровки: "))
array = ''
for i in string:
i = ord(i)
i += key
i = chr(i)
array += i
print(array)
А то у Вас, как уже заметил
nukler в комментариях, в каждую итерацию, результат обнуляется. Ну и, конечно, вывод результата - единожды, в конце, а не после каждой буквы.
В функции deciph() - тоже самое с выносом из тела цикла, но тут (в варианте со списком) ''.join(array) как раз пригодится вместо ошибочной "array[array]". Но использование списка в данном случае избыточно (разве что с образовательной т.з.)