mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-07-01 09:43:37 +00:00
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
fe16e4d9c4 | |||
1caf65c68d | |||
85eb4983bc | |||
5a2d3ecc2e | |||
32f0ed88f4 | |||
d35c58509b | |||
f810b185f0 | |||
5ef1fe996b | |||
2b5fdf541d |
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -45,9 +45,12 @@ jobs:
|
||||
- name: Update Build Number
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
|
||||
sed -i '' -e "s/CI_VERSION = 0.0.0/CI_VERSION = $CLEAN_TAG/g" Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/g" Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Config/Config.xcconfig
|
||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Secretive/Credits.rtf
|
||||
- name: Build
|
||||
run: xcrun xcodebuild -project Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||
- name: Create ZIPs
|
||||
|
@ -1 +1,2 @@
|
||||
CI_VERSION = 0.0.0
|
||||
CI_VERSION = GITHUB_CI_VERSION
|
||||
CI_BUILD_NUMBER = GITHUB_BUILD_NUMBER
|
||||
|
@ -14,7 +14,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}()
|
||||
let notifier = Notifier()
|
||||
lazy var agent: Agent = {
|
||||
Agent(storeList: storeList/*, notifier: notifier*/)
|
||||
Agent(storeList: storeList, witness: notifier)
|
||||
}()
|
||||
lazy var socketController: SocketController = {
|
||||
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
||||
|
@ -11,11 +11,11 @@ class Notifier {
|
||||
}
|
||||
}
|
||||
|
||||
func notify(accessTo secret: AnySecret) {
|
||||
func notify(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) {
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
let notificationContent = UNMutableNotificationContent()
|
||||
notificationContent.title = "Signed Request"
|
||||
notificationContent.body = "\(secret.name) was used to sign a request."
|
||||
notificationContent.body = "\(secret.name) was used to sign a request from \(provenance.origin.name)."
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
|
||||
notificationCenter.add(request, withCompletionHandler: nil)
|
||||
}
|
||||
@ -24,8 +24,11 @@ class Notifier {
|
||||
|
||||
extension Notifier: SigningWitness {
|
||||
|
||||
func witness(accessTo secret: AnySecret) throws {
|
||||
notify(accessTo: secret)
|
||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||
}
|
||||
|
||||
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||
notify(accessTo: secret, by: provenance)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,12 +2,15 @@ import Foundation
|
||||
import CryptoKit
|
||||
import OSLog
|
||||
import SecretKit
|
||||
import SecretAgentKit
|
||||
import AppKit
|
||||
|
||||
public class Agent {
|
||||
|
||||
fileprivate let storeList: SecretStoreList
|
||||
fileprivate let witness: SigningWitness?
|
||||
fileprivate let writer = OpenSSHKeyWriter()
|
||||
fileprivate let requestTracer = SigningRequestTracer()
|
||||
|
||||
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
||||
os_log(.debug, "Agent is running")
|
||||
@ -40,7 +43,7 @@ extension Agent {
|
||||
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)
|
||||
case .signRequest:
|
||||
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||
response.append(try sign(data: data))
|
||||
response.append(try sign(data: data, from: fileHandle.fileDescriptor))
|
||||
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentSignResponse.debugDescription)
|
||||
}
|
||||
} catch {
|
||||
@ -74,7 +77,7 @@ extension Agent {
|
||||
return countData + keyData
|
||||
}
|
||||
|
||||
func sign(data: Data) throws -> Data {
|
||||
func sign(data: Data, from pid: Int32) throws -> Data {
|
||||
let reader = OpenSSHReader(data: data)
|
||||
let hash = try reader.readNextChunk()
|
||||
guard let (store, secret) = secret(matching: hash) else {
|
||||
@ -82,8 +85,9 @@ extension Agent {
|
||||
throw AgentError.noMatchingKey
|
||||
}
|
||||
|
||||
let provenance = requestTracer.provenance(from: pid)
|
||||
if let witness = witness {
|
||||
try witness.witness(accessTo: secret)
|
||||
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, by: provenance)
|
||||
}
|
||||
|
||||
let dataToSign = try reader.readNextChunk()
|
||||
@ -118,6 +122,10 @@ extension Agent {
|
||||
sub.append(writer.lengthAndData(of: signatureChunk))
|
||||
signedData.append(writer.lengthAndData(of: sub))
|
||||
|
||||
if let witness = witness {
|
||||
try witness.witness(accessTo: secret, by: provenance)
|
||||
}
|
||||
|
||||
os_log(.debug, "Agent signed request")
|
||||
|
||||
return signedData
|
||||
|
@ -7,6 +7,16 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
|
||||
// Forward declarations
|
||||
|
||||
// from libproc.h
|
||||
int proc_pidpath(int pid, void * buffer, uint32_t buffersize);
|
||||
|
||||
// from SecTask.h
|
||||
OSStatus SecCodeCreateWithPID(int32_t, SecCSFlags, SecCodeRef *);
|
||||
|
||||
//! Project version number for SecretAgentKit.
|
||||
FOUNDATION_EXPORT double SecretAgentKitVersionNumber;
|
||||
@ -14,6 +24,4 @@ FOUNDATION_EXPORT double SecretAgentKitVersionNumber;
|
||||
//! Project version string for SecretAgentKit.
|
||||
FOUNDATION_EXPORT const unsigned char SecretAgentKitVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <SecretAgentKit/PublicHeader.h>
|
||||
|
||||
|
||||
|
45
SecretAgentKit/SigningRequestProvenance.swift
Normal file
45
SecretAgentKit/SigningRequestProvenance.swift
Normal file
@ -0,0 +1,45 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
public struct SigningRequestProvenance {
|
||||
|
||||
public var chain: [Process]
|
||||
public init(root: Process) {
|
||||
self.chain = [root]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SigningRequestProvenance {
|
||||
|
||||
public var origin: Process {
|
||||
chain.last!
|
||||
}
|
||||
|
||||
public var intact: Bool {
|
||||
return chain.reduce(true) { $0 && $1.validSignature }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SigningRequestProvenance {
|
||||
|
||||
public struct Process {
|
||||
|
||||
public let pid: Int32
|
||||
public let name: String
|
||||
public let path: String
|
||||
public let validSignature: Bool
|
||||
let parentPID: Int32?
|
||||
|
||||
init(pid: Int32, name: String, path: String, validSignature: Bool, parentPID: Int32?) {
|
||||
self.pid = pid
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.validSignature = validSignature
|
||||
self.parentPID = parentPID
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
43
SecretAgentKit/SigningRequestTracer.swift
Normal file
43
SecretAgentKit/SigningRequestTracer.swift
Normal file
@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Security
|
||||
|
||||
struct SigningRequestTracer {
|
||||
|
||||
func provenance(from pid: Int32) -> SigningRequestProvenance {
|
||||
let pidPointer = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1)
|
||||
var len = socklen_t(MemoryLayout<Int32>.size)
|
||||
getsockopt(pid, SOCK_STREAM, LOCAL_PEERPID, pidPointer, &len)
|
||||
let pid = pidPointer.load(as: Int32.self)
|
||||
let firstInfo = process(from: pid)
|
||||
|
||||
var provenance = SigningRequestProvenance(root: firstInfo)
|
||||
while NSRunningApplication(processIdentifier: provenance.origin.pid) == nil && provenance.origin.parentPID != nil {
|
||||
provenance.chain.append(process(from: provenance.origin.parentPID!))
|
||||
}
|
||||
return provenance
|
||||
}
|
||||
|
||||
func pidAndNameInfo(from pid: Int32) -> kinfo_proc {
|
||||
var len = MemoryLayout<kinfo_proc>.size
|
||||
let infoPointer = UnsafeMutableRawPointer.allocate(byteCount: len, alignment: 1)
|
||||
var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]
|
||||
sysctl(&name, UInt32(name.count), infoPointer, &len, nil, 0)
|
||||
return infoPointer.load(as: kinfo_proc.self)
|
||||
}
|
||||
|
||||
func process(from pid: Int32) -> SigningRequestProvenance.Process {
|
||||
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
|
||||
let ppid = pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
|
||||
let procName = String(cString: &pidAndNameInfo.kp_proc.p_comm.0)
|
||||
let pathPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(MAXPATHLEN))
|
||||
_ = proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
|
||||
let path = String(cString: pathPointer)
|
||||
var secCode: Unmanaged<SecCode>!
|
||||
let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks]
|
||||
SecCodeCreateWithPID(pid, SecCSFlags(), &secCode)
|
||||
let valid = SecCodeCheckValidity(secCode.takeRetainedValue(), flags, nil) == errSecSuccess
|
||||
return SigningRequestProvenance.Process(pid: pid, name: procName, path: path, validSignature: valid, parentPID: ppid)
|
||||
}
|
||||
|
||||
}
|
@ -3,6 +3,7 @@ import SecretKit
|
||||
|
||||
public protocol SigningWitness {
|
||||
|
||||
func witness(accessTo secret: AnySecret) throws
|
||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
|
||||
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
|
||||
|
||||
}
|
||||
|
@ -11,16 +11,17 @@ extension SmartCard {
|
||||
// TODO: Read actual smart card name, eg "YubiKey 5c"
|
||||
@Published public var isAvailable: Bool = false
|
||||
public let id = UUID()
|
||||
public let name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
||||
public fileprivate(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
||||
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||
fileprivate let watcher = TKTokenWatcher()
|
||||
fileprivate var tokenID: String?
|
||||
|
||||
public init() {
|
||||
tokenID = watcher.tokenIDs.filter { !$0.contains("setoken") }.first
|
||||
tokenID = watcher.nonSecureEnclaveTokens.first
|
||||
watcher.setInsertionHandler { string in
|
||||
guard self.tokenID == nil else { return }
|
||||
guard !string.contains("setoken") else { return }
|
||||
|
||||
self.tokenID = string
|
||||
self.reloadSecrets()
|
||||
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
|
||||
@ -97,6 +98,14 @@ extension SmartCard.Store {
|
||||
|
||||
fileprivate func loadSecrets() {
|
||||
guard let tokenID = tokenID else { return }
|
||||
// Hack to read name if there's only one smart card
|
||||
let slotNames = TKSmartCardSlotManager().slotNames
|
||||
if watcher.nonSecureEnclaveTokens.count == 1 && slotNames.count == 1 {
|
||||
name = slotNames.first!
|
||||
} else {
|
||||
name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
||||
}
|
||||
|
||||
let attributes = [
|
||||
kSecClass: kSecClassKey,
|
||||
kSecAttrTokenID: tokenID,
|
||||
@ -124,6 +133,14 @@ extension SmartCard.Store {
|
||||
|
||||
}
|
||||
|
||||
extension TKTokenWatcher {
|
||||
|
||||
fileprivate var nonSecureEnclaveTokens: [String] {
|
||||
tokenIDs.filter { !$0.contains("setoken") }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SmartCard {
|
||||
|
||||
public struct KeychainError: Error {
|
||||
|
@ -26,6 +26,7 @@
|
||||
50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; };
|
||||
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; };
|
||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
||||
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
|
||||
5068389E241471CD00F55094 /* SecretStoreList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5068389D241471CD00F55094 /* SecretStoreList.swift */; };
|
||||
506838A12415EA5600F55094 /* AnySecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A02415EA5600F55094 /* AnySecret.swift */; };
|
||||
506838A32415EA5D00F55094 /* AnySecretStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A22415EA5D00F55094 /* AnySecretStore.swift */; };
|
||||
@ -35,6 +36,8 @@
|
||||
507CE4ED2420A3C70029F750 /* Agent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79F24026B9900D209EA /* Agent.swift */; };
|
||||
507CE4EE2420A3CA0029F750 /* SocketController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79D24026B9900D209EA /* SocketController.swift */; };
|
||||
507CE4F02420A4C50029F750 /* SigningWitness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4EF2420A4C50029F750 /* SigningWitness.swift */; };
|
||||
507CE4F42420A8C10029F750 /* SigningRequestProvenance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */; };
|
||||
507CE4F62420A96F0029F750 /* SigningRequestTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */; };
|
||||
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */; };
|
||||
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */; };
|
||||
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.swift */; };
|
||||
@ -197,12 +200,15 @@
|
||||
50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = "<group>"; };
|
||||
50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; };
|
||||
50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = "<group>"; };
|
||||
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
|
||||
5068389D241471CD00F55094 /* SecretStoreList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreList.swift; sourceTree = "<group>"; };
|
||||
506838A02415EA5600F55094 /* AnySecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecret.swift; sourceTree = "<group>"; };
|
||||
506838A22415EA5D00F55094 /* AnySecretStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecretStore.swift; sourceTree = "<group>"; };
|
||||
50731665241DF8660023809E /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = "<group>"; };
|
||||
50731668241E00C20023809E /* NoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeView.swift; sourceTree = "<group>"; };
|
||||
507CE4EF2420A4C50029F750 /* SigningWitness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningWitness.swift; sourceTree = "<group>"; };
|
||||
507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestProvenance.swift; sourceTree = "<group>"; };
|
||||
507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestTracer.swift; sourceTree = "<group>"; };
|
||||
508A58A9241E06B40069DC07 /* PreviewUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewUpdater.swift; sourceTree = "<group>"; };
|
||||
508A58AB241E121B0069DC07 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
|
||||
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusChecker.swift; sourceTree = "<group>"; };
|
||||
@ -333,6 +339,7 @@
|
||||
50617D8B23FCE48E0099B055 /* Main.storyboard */,
|
||||
50617D8E23FCE48E0099B055 /* Info.plist */,
|
||||
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
|
||||
506772C62424784600034DED /* Credits.rtf */,
|
||||
50617D8823FCE48E0099B055 /* Preview Content */,
|
||||
);
|
||||
path = Secretive;
|
||||
@ -468,6 +475,8 @@
|
||||
5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */,
|
||||
50A3B79D24026B9900D209EA /* SocketController.swift */,
|
||||
507CE4EF2420A4C50029F750 /* SigningWitness.swift */,
|
||||
507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */,
|
||||
507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */,
|
||||
50A3B79F24026B9900D209EA /* Agent.swift */,
|
||||
5099A06F240242BA0062B6F2 /* Info.plist */,
|
||||
);
|
||||
@ -735,6 +744,7 @@
|
||||
50617D8D23FCE48E0099B055 /* Main.storyboard in Resources */,
|
||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
|
||||
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */,
|
||||
506772C72424784600034DED /* Credits.rtf in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -850,6 +860,8 @@
|
||||
5099A08A240242C20062B6F2 /* SSHAgentProtocol.swift in Sources */,
|
||||
507CE4ED2420A3C70029F750 /* Agent.swift in Sources */,
|
||||
507CE4F02420A4C50029F750 /* SigningWitness.swift in Sources */,
|
||||
507CE4F62420A96F0029F750 /* SigningRequestTracer.swift in Sources */,
|
||||
507CE4F42420A8C10029F750 /* SigningRequestProvenance.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -67,7 +67,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Test"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
|
23
Secretive/Credits.rtf
Normal file
23
Secretive/Credits.rtf
Normal file
@ -0,0 +1,23 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2511
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
{\*\expandedcolortbl;;}
|
||||
\margl1440\margr1440\vieww9000\viewh8400\viewkind0
|
||||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6119\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive"}}{\fldrslt
|
||||
\f0\fs24 \cf0 https://github.com/maxgoedjen/secretive}}
|
||||
\f0\fs24 \
|
||||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
||||
\cf0 \
|
||||
GITHUB_BUILD_URL\
|
||||
\
|
||||
Special Thanks To:\
|
||||
https://github.com/bdash\
|
||||
https://github.com/danielctull\
|
||||
https://github.com/davedelong\
|
||||
https://github.com/esttorhe\
|
||||
https://github.com/joeblau\
|
||||
https://github.com/marksands\
|
||||
https://github.com/mergesort\
|
||||
https://github.com/phillco\
|
||||
https://github.com/zackdotcomputer}
|
Reference in New Issue
Block a user